diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index c3331ebe40..dfa0945890 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -64,6 +64,7 @@ "tests\\Avalonia.Headless.XUnit.PerAssembly.UnitTests\\Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj", "tests\\Avalonia.Headless.XUnit.PerTest.UnitTests\\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "tests\\Avalonia.IntegrationTests.Appium\\Avalonia.IntegrationTests.Appium.csproj", + "tests\\Avalonia.IntegrationTests.Win32\\Avalonia.IntegrationTests.Win32.csproj", "tests\\Avalonia.LeakTests\\Avalonia.LeakTests.csproj", "tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj", "tests\\Avalonia.Markup.Xaml.UnitTests\\Avalonia.Markup.Xaml.UnitTests.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index b4b89de2a3..207f673c26 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -118,14 +118,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Designer.HostApp", "src\tools\Avalonia.Designer.HostApp\Avalonia.Designer.HostApp.csproj", "{050CC912-FF49-4A8B-B534-9544017446DD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Previewer", "samples\Previewer\Previewer.csproj", "{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.OpenGL", "src\Avalonia.OpenGL\Avalonia.OpenGL.csproj", "{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}" @@ -211,6 +207,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NOTICE.md = NOTICE.md NuGet.Config = NuGet.Config readme.md = readme.md + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" @@ -407,18 +404,10 @@ Global {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.Build.0 = Release|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Release|Any CPU.Build.0 = Release|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.Build.0 = Release|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Release|Any CPU.Build.0 = Release|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -710,9 +699,7 @@ Global {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} {050CC912-FF49-4A8B-B534-9544017446DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} diff --git a/Directory.Packages.props b/Directory.Packages.props index 96d6f314d8..58650e1729 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,14 +7,14 @@ - + - - - + + + @@ -46,9 +46,9 @@ - - - + + + @@ -59,7 +59,7 @@ - + diff --git a/api/Avalonia.LinuxFramebuffer.nupkg.xml b/api/Avalonia.LinuxFramebuffer.nupkg.xml new file mode 100644 index 0000000000..10c927a203 --- /dev/null +++ b/api/Avalonia.LinuxFramebuffer.nupkg.xml @@ -0,0 +1,40 @@ + + + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.CreateFramebufferRenderTarget + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.Lock + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.CreateFramebufferRenderTarget + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.Lock + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + + CP0008 + T:Avalonia.LinuxFramebuffer.FbdevOutput + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0008 + T:Avalonia.LinuxFramebuffer.FbdevOutput + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + \ No newline at end of file diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index 8e9d60f7d4..c1afe2f966 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,6 +1,12 @@ - + + + CP0001 + T:Avalonia.Skia.ISkiaGpu + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0001 T:Avalonia.Skia.ISkiaGpuRenderTarget2 @@ -13,6 +19,12 @@ baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0001 + T:Avalonia.Skia.ISkiaGpu + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0001 T:Avalonia.Skia.ISkiaGpuRenderTarget2 @@ -25,24 +37,72 @@ baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.Helpers.DrawingContextHelper.WrapSkiaCanvas(SkiaSharp.SKCanvas,Avalonia.Vector) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0002 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.Helpers.DrawingContextHelper.WrapSkiaCanvas(SkiaSharp.SKCanvas,Avalonia.Vector) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0002 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + + CP0006 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpu.TryGetGrContext baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) @@ -55,12 +115,24 @@ baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpu.TryGetGrContext baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) @@ -97,4 +169,4 @@ baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll - + \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index aee61ea399..92f41c6606 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -103,6 +103,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.Gestures + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Input.IDataObject @@ -211,6 +217,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Utilities.MathUtilities + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Utilities.StringTokenizer @@ -241,6 +253,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Chrome.CaptionButtons + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Chrome.TitleBar + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.ContextRequestedEventArgs + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Diagnostics.IPopupHostProvider @@ -301,6 +331,42 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FramebufferLockProperties + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FuncFramebufferRenderTarget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferPlatformSurface + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTarget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTargetWithProperties + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Primitives.ChromeOverlayLayer @@ -331,18 +397,42 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Remote.RemoteServer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteWidget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SaveFileDialog baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.SystemDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SystemDialog baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Platform.IApplicationPlatformEvents @@ -475,6 +565,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.Gestures + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Input.IDataObject @@ -583,6 +679,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Utilities.MathUtilities + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Utilities.StringTokenizer @@ -613,6 +715,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Chrome.CaptionButtons + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Chrome.TitleBar + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.ContextRequestedEventArgs + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Diagnostics.IPopupHostProvider @@ -673,6 +793,42 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FramebufferLockProperties + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FuncFramebufferRenderTarget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferPlatformSurface + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTarget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTargetWithProperties + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Primitives.ChromeOverlayLayer @@ -703,18 +859,42 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Remote.RemoteServer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteWidget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SaveFileDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.SystemDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SystemDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Platform.IApplicationPlatformEvents @@ -775,6 +955,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + F:Avalonia.Input.HoldingState.Cancelled + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 F:Avalonia.Media.DrawingImage.ViewboxProperty @@ -817,6 +1003,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamObservable + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamTask + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.TypeCast(System.Type) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Data.ReflectionBinding.#ctor(System.String,Avalonia.Data.BindingMode) @@ -865,6 +1069,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -909,313 +1119,469 @@ CP0002 - M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + M:Avalonia.Input.InputElement.AddPinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) + M:Avalonia.Input.InputElement.AddPinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetTextAsync + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) + M:Avalonia.Input.InputElement.AddPullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync + M:Avalonia.Input.InputElement.AddPullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) + M:Avalonia.Input.InputElement.AddScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) + M:Avalonia.Input.InputElement.AddScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.get_Data + M:Avalonia.Input.InputElement.AddScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) + M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) + M:Avalonia.Input.InputElement.RemovePinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Color.ToUint32 + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) + M:Avalonia.Input.InputElement.RemovePullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) + M:Avalonia.Input.InputElement.RemovePullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushTransformContainer + M:Avalonia.Input.InputElement.RemoveScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.get_Viewbox + M:Avalonia.Input.InputElement.RemoveScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) + M:Avalonia.Input.InputElement.RemoveScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) + M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius + M:Avalonia.Input.Platform.IClipboard.GetTextAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.IRadialGradientBrush.get_Radius + M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.get_Radius + M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Input.Raw.RawDragEvent.get_Data baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) + M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Layout.LayoutHelper.RoundLayoutSizeUp(Avalonia.Size,System.Double,System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Layout.LayoutHelper.RoundLayoutThickness(Avalonia.Thickness,System.Double,System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Media.Color.ToUint32 baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Media.DrawingContext.PushTransformContainer baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Media.DrawingImage.get_Viewbox baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) + M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted + M:Avalonia.Media.IRadialGradientBrush.get_Radius baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) + M:Avalonia.Media.RadialGradientBrush.get_Radius baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Rendering.SceneInvalidatedEventArgs.get_RenderRoot + M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) + M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visual.get_VisualRoot + M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Platform.IBitmapImpl,Avalonia.PixelPoint) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SceneInvalidatedEventArgs.get_RenderRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visual.get_VisualRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll @@ -1261,6 +1627,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Control.ContextRequestedEvent + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.Documents.Inline.TextDecorationsProperty @@ -1327,6 +1699,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.SystemDecorationsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.AppBuilder.get_LifetimeOverride @@ -1435,6 +1819,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Generators.ItemContainerGenerator.ContainerFromIndex(System.Int32) @@ -1699,6 +2089,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.get_SystemDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) @@ -1711,12 +2119,30 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.ITopLevelImpl.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetSystemDecorations(Avalonia.Controls.SystemDecorations) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) @@ -1807,18 +2233,66 @@ baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0002 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0002 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + CP0002 F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache @@ -1879,6 +2353,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + F:Avalonia.Input.HoldingState.Cancelled + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 F:Avalonia.Media.DrawingImage.ViewboxProperty @@ -1921,6 +2401,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamObservable + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamTask + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.TypeCast(System.Type) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Data.ReflectionBinding.#ctor(System.String,Avalonia.Data.BindingMode) @@ -1969,6 +2467,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -2011,6 +2515,126 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.InputElement.AddPinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddPullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.AddScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemovePullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemoveScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemoveScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.InputElement.RemoveScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) @@ -2095,6 +2719,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Layout.LayoutHelper.RoundLayoutSizeUp(Avalonia.Size,System.Double,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Layout.LayoutHelper.RoundLayoutThickness(Avalonia.Thickness,System.Double,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Media.Color.ToUint32 @@ -2239,6 +2875,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Platform.IBitmapImpl,Avalonia.PixelPoint) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) @@ -2281,6 +2923,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) @@ -2365,6 +3025,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Control.ContextRequestedEvent + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.Documents.Inline.TextDecorationsProperty @@ -2431,6 +3097,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.SystemDecorationsProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.AppBuilder.get_LifetimeOverride @@ -2539,6 +3217,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Generators.ItemContainerGenerator.ContainerFromIndex(System.Int32) @@ -2803,6 +3487,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.get_SystemDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) @@ -2815,12 +3517,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.ITopLevelImpl.get_Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetSystemDecorations(Avalonia.Controls.SystemDecorations) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) @@ -2901,25 +3621,49 @@ CP0002 - M:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.#ctor(System.String,Avalonia.Data.BindingMode) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + M:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.#ctor(System.String,Avalonia.Data.BindingMode) + baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + + + CP0002 + M:Avalonia.Markup.Xaml.XamlLoadException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) + baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll CP0002 - M:Avalonia.Markup.Xaml.XamlLoadException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) - baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll CP0002 - M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,System.Object) baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll CP0002 - M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,System.Object) baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll @@ -2929,6 +3673,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + CP0002 F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache @@ -2971,12 +3739,48 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0005 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0005 + P:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0005 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0005 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0005 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0005 + P:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0005 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0005 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) @@ -2989,6 +3793,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IDrawingContextImpl.PopTextOptions @@ -3007,6 +3817,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IDrawingContextLayerImpl.CreateDrawingContext + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) @@ -3043,18 +3859,42 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IRenderTargetBitmapImpl.CreateDrawingContext + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Input.IInputRoot.FocusRoot baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IDrawingContextLayerImpl.IsCorrupted + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat @@ -3079,12 +3919,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IWindowImpl.SetWindowDecorations(Avalonia.Controls.WindowDecorations) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0006 M:Avalonia.Platform.IWindowingPlatform.GetWindowsZOrder(System.ReadOnlySpan{Avalonia.Platform.IWindowImpl},System.Span{System.Int64}) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Platform.ITopLevelImpl.Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0006 + P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0006 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) @@ -3097,6 +3973,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -3187,6 +4081,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IDrawingContextImpl.PopTextOptions @@ -3205,6 +4105,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IDrawingContextLayerImpl.CreateDrawingContext + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) @@ -3241,6 +4147,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -3253,6 +4165,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IRenderTargetBitmapImpl.CreateDrawingContext + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) @@ -3265,6 +4189,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IDrawingContextLayerImpl.IsCorrupted + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat @@ -3289,12 +4219,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IWindowImpl.SetWindowDecorations(Avalonia.Controls.WindowDecorations) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0006 M:Avalonia.Platform.IWindowingPlatform.GetWindowsZOrder(System.ReadOnlySpan{Avalonia.Platform.IWindowImpl},System.Span{System.Int64}) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Platform.ITopLevelImpl.Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0006 + P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) @@ -3307,6 +4255,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0006 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) @@ -3325,6 +4291,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -3421,6 +4405,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerWithRenderContextAffinityImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IPlatformGraphicsContext @@ -3439,6 +4435,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IRenderTargetBitmapImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IWriteableBitmapImpl @@ -3589,6 +4591,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerWithRenderContextAffinityImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IPlatformGraphicsContext @@ -3607,6 +4621,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IRenderTargetBitmapImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IWriteableBitmapImpl @@ -3745,6 +4765,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0009 + T:Avalonia.Input.HoldingRoutedEventArgs + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0009 T:Avalonia.Controls.Primitives.AdornerLayer @@ -3781,6 +4807,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0009 + T:Avalonia.Input.HoldingRoutedEventArgs + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0009 T:Avalonia.Controls.Primitives.AdornerLayer diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 940699f09d..353ee4d2f1 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -62,8 +62,6 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetExtendClientArea (bool enable) override; - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override; - virtual HRESULT GetExtendTitleBarHeight (double*ret) override; virtual HRESULT SetExtendTitleBarHeight (double value) override; @@ -110,7 +108,6 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; - AvnExtendClientAreaChromeHints _extendClientHints; bool _isModal; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 5a57715b55..1d77d3ce44 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -13,7 +13,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowBaseImpl(events, false) { _isEnabled = true; _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _canMinimize = true; @@ -153,20 +152,11 @@ void WindowImpl::WindowStateChanged() { if (_isClientAreaExtended) { if (_lastWindowState == FullScreen) { // we exited fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - [Window setTitlebarAppearsTransparent:true]; [StandardContainer setFrameSize:StandardContainer.frame.size]; } else if (state == FullScreen) { // we entered fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = nullptr; - } - [Window setTitlebarAppearsTransparent:false]; [StandardContainer setFrameSize:StandardContainer.frame.size]; @@ -240,6 +230,10 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateAppearance(); + if (_isClientAreaExtended) { + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; + } + switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -391,20 +385,9 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { [Window setTitlebarAppearsTransparent:true]; - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) { - [StandardContainer ShowTitleBar:true]; - } else { - [StandardContainer ShowTitleBar:false]; - } + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } else { - Window.toolbar = nullptr; - } + Window.toolbar = nullptr; } else { Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; @@ -420,17 +403,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } } -HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { - START_COM_CALL; - - @autoreleasepool { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } -} - HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { START_COM_CALL; @@ -619,9 +591,7 @@ void WindowImpl::UpdateAppearance() { return; } - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = (_decorations == SystemDecorationsFull) && - (_isClientAreaExtended ? wantsChrome : true); + bool hasTrafficLights = (_decorations == SystemDecorationsFull); NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; diff --git a/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg b/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg new file mode 100644 index 0000000000..b7b9610f95 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg b/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg new file mode 100644 index 0000000000..2bb09f1185 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg new file mode 100644 index 0000000000..4447a8a6f2 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg new file mode 100644 index 0000000000..58acb3ebf0 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg new file mode 100644 index 0000000000..4722989113 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg new file mode 100644 index 0000000000..d434d6194b Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg new file mode 100644 index 0000000000..db35f09e02 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg new file mode 100644 index 0000000000..0859309904 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/avatar.jpg b/samples/ControlCatalog/Assets/ModernApp/avatar.jpg new file mode 100644 index 0000000000..3c55a5af75 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg new file mode 100644 index 0000000000..ebbac8920b Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg new file mode 100644 index 0000000000..876be96909 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg new file mode 100644 index 0000000000..aec9e2ef36 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg b/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg new file mode 100644 index 0000000000..812859a392 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg b/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg new file mode 100644 index 0000000000..77f73a4af1 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg new file mode 100644 index 0000000000..d8b2fce6d8 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg new file mode 100644 index 0000000000..605f072dfe Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg new file mode 100644 index 0000000000..788d63cce9 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg new file mode 100644 index 0000000000..7e04fcdeca Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg new file mode 100644 index 0000000000..a27ef779a5 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg new file mode 100644 index 0000000000..fcc67fa623 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story1.jpg b/samples/ControlCatalog/Assets/ModernApp/story1.jpg new file mode 100644 index 0000000000..b75847fb11 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story1.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story2.jpg b/samples/ControlCatalog/Assets/ModernApp/story2.jpg new file mode 100644 index 0000000000..664c7ef7ef Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story2.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story3.jpg b/samples/ControlCatalog/Assets/ModernApp/story3.jpg new file mode 100644 index 0000000000..b949cf63c3 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story3.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/cast1.jpg b/samples/ControlCatalog/Assets/Movies/cast1.jpg new file mode 100644 index 0000000000..2eb838400e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/cast1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/cast2.jpg b/samples/ControlCatalog/Assets/Movies/cast2.jpg new file mode 100644 index 0000000000..42607923c8 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/cast2.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/continue1.jpg b/samples/ControlCatalog/Assets/Movies/continue1.jpg new file mode 100644 index 0000000000..27457bfe79 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/continue1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/hero.jpg b/samples/ControlCatalog/Assets/Movies/hero.jpg new file mode 100644 index 0000000000..f48e206e2e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/hero.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/morelike1.jpg b/samples/ControlCatalog/Assets/Movies/morelike1.jpg new file mode 100644 index 0000000000..62852e0d8a Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/morelike1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/search1.jpg b/samples/ControlCatalog/Assets/Movies/search1.jpg new file mode 100644 index 0000000000..17cb2fe685 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/search1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated1.jpg b/samples/ControlCatalog/Assets/Movies/toprated1.jpg new file mode 100644 index 0000000000..f5de43613e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated2.jpg b/samples/ControlCatalog/Assets/Movies/toprated2.jpg new file mode 100644 index 0000000000..c54cbd5b34 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated2.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated3.jpg b/samples/ControlCatalog/Assets/Movies/toprated3.jpg new file mode 100644 index 0000000000..c78f4d3278 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated3.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated4.jpg b/samples/ControlCatalog/Assets/Movies/toprated4.jpg new file mode 100644 index 0000000000..f70cd0d283 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated4.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/trending1.jpg b/samples/ControlCatalog/Assets/Movies/trending1.jpg new file mode 100644 index 0000000000..b208d69e33 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/trending1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/trending2.jpg b/samples/ControlCatalog/Assets/Movies/trending2.jpg new file mode 100644 index 0000000000..44fcce2e1b Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/trending2.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg b/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg new file mode 100644 index 0000000000..010d1c5162 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg b/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg new file mode 100644 index 0000000000..bd63302eee Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg b/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg new file mode 100644 index 0000000000..64db513b0b Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg b/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg new file mode 100644 index 0000000000..a188abfa58 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg b/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg new file mode 100644 index 0000000000..b6a272f9f5 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg b/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg new file mode 100644 index 0000000000..c516f0d2a8 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg b/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg new file mode 100644 index 0000000000..9086115000 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg b/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg new file mode 100644 index 0000000000..449be4bcb2 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg b/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg new file mode 100644 index 0000000000..0b6b907926 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg b/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg new file mode 100644 index 0000000000..7d79ec36b0 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg b/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg new file mode 100644 index 0000000000..be573b35a7 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg b/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg new file mode 100644 index 0000000000..a72c8e9159 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg b/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg new file mode 100644 index 0000000000..d2fb1dfe91 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish1.jpg b/samples/ControlCatalog/Assets/Restaurant/dish1.jpg new file mode 100644 index 0000000000..fa47be4b8a Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish1.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish2.jpg b/samples/ControlCatalog/Assets/Restaurant/dish2.jpg new file mode 100644 index 0000000000..bdcbfb656f Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish2.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish3.jpg b/samples/ControlCatalog/Assets/Restaurant/dish3.jpg new file mode 100644 index 0000000000..9f4f906f01 Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish3.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish4.jpg b/samples/ControlCatalog/Assets/Restaurant/dish4.jpg new file mode 100644 index 0000000000..2bbaf1db9d Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish4.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg b/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg new file mode 100644 index 0000000000..ad21a354dd Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg b/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg new file mode 100644 index 0000000000..664c7ef7ef Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg b/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg new file mode 100644 index 0000000000..e262425fff Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg b/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg new file mode 100644 index 0000000000..6b8967fc83 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg b/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg new file mode 100644 index 0000000000..2192a3a72c Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/hero.jpg b/samples/ControlCatalog/Assets/RetroGaming/hero.jpg new file mode 100644 index 0000000000..6a265a5ef7 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/hero.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg b/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg new file mode 100644 index 0000000000..6a5994ec45 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg b/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg new file mode 100644 index 0000000000..68c973ba93 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg b/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg new file mode 100644 index 0000000000..9af86cdbb3 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg b/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg new file mode 100644 index 0000000000..b39a00967f Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c71e7a93ad..8304e3e002 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -13,6 +13,12 @@ + + + + + + diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index 997ae54f41..804eebfb40 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" - SystemDecorations="None" Name="Window"> + WindowDecorations="None" Name="Window"> @@ -43,11 +43,11 @@ Hello world! - + - None - BorderOnly - Full + None + BorderOnly + Full CanResize diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 20b7d33c60..c8c496b50c 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -49,9 +49,15 @@ + + + + @@ -67,6 +73,11 @@ + + + @@ -99,6 +110,11 @@ + + + @@ -128,6 +144,11 @@ + + + @@ -167,6 +188,11 @@ + + + @@ -218,9 +244,9 @@ SelectionChanged="Decorations_SelectionChanged" ToolTip.Tip="System Decorations"> - None - BorderOnly - Full + None + BorderOnly + Full 0 && e.AddedItems[0] is SystemDecorations systemDecorations) + if (TopLevel.GetTopLevel(this) is Window window && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowDecorations systemDecorations) { - window.SystemDecorations = systemDecorations; + window.WindowDecorations = systemDecorations; } } @@ -79,7 +79,7 @@ namespace ControlCatalog base.OnAttachedToVisualTree(e); if (TopLevel.GetTopLevel(this) is Window window) - Decorations.SelectedIndex = (int)window.SystemDecorations; + Decorations.SelectedIndex = (int)window.WindowDecorations; var insets = TopLevel.GetTopLevel(this)!.InsetsManager; if (insets != null) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ebf6ed9a0f..b466315878 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,6 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels;assembly=ControlCatalog" xmlns:v="using:ControlCatalog.Views" ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" - ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" CanResize="{Binding CanResize}" CanMinimize="{Binding CanMinimize}" diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml new file mode 100644 index 0000000000..1ea3349129 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml @@ -0,0 +1,120 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Customize the CommandBar appearance using Background, Foreground, BorderBrush, and CornerRadius. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs new file mode 100644 index 0000000000..52c98757f0 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs @@ -0,0 +1,90 @@ +using Avalonia.Controls; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarCustomizationPage : UserControl + { + public CommandBarCustomizationPage() + { + InitializeComponent(); + } + + private void OnBgPresetChanged(object? sender, SelectionChangedEventArgs e) + { + if (LiveBar == null) + return; + + switch (BgPresetCombo.SelectedIndex) + { + case 1: + LiveBar.Background = new SolidColorBrush(Color.Parse("#0078D4")); + break; + case 2: + LiveBar.Background = new SolidColorBrush(Color.Parse("#1C1C1E")); + break; + case 3: + LiveBar.Background = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 0, Avalonia.RelativeUnit.Relative), + GradientStops = + { + new GradientStop(Color.Parse("#3F51B5"), 0), + new GradientStop(Color.Parse("#E91E63"), 1) + } + }; + break; + case 4: + LiveBar.Background = Brushes.Transparent; + break; + default: + LiveBar.ClearValue(BackgroundProperty); + break; + } + } + + private void OnFgChanged(object? sender, SelectionChangedEventArgs e) + { + if (LiveBar == null) + return; + + switch (FgCombo.SelectedIndex) + { + case 1: + LiveBar.Foreground = Brushes.White; + break; + case 2: + LiveBar.Foreground = Brushes.Black; + break; + default: + LiveBar.ClearValue(ForegroundProperty); + break; + } + } + + private void OnRadiusChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (LiveBar == null) + return; + + var r = (int)RadiusSlider.Value; + LiveBar.CornerRadius = new Avalonia.CornerRadius(r); + RadiusLabel.Text = $"{r}"; + } + + private void OnBorderChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (LiveBar == null) + return; + + var t = (int)BorderSlider.Value; + LiveBar.BorderThickness = new Avalonia.Thickness(t); + BorderLabel.Text = $"{t}"; + if (t > 0) + LiveBar.BorderBrush = Brushes.Gray; + else + LiveBar.BorderBrush = null; + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml new file mode 100644 index 0000000000..2f771ab42e --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml @@ -0,0 +1,75 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z + M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs new file mode 100644 index 0000000000..45a1c95527 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs @@ -0,0 +1,45 @@ +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarDynamicOverflowPage : UserControl + { + public CommandBarDynamicOverflowPage() + { + InitializeComponent(); + ((INotifyCollectionChanged)DemoBar.OverflowItems).CollectionChanged += OnOverflowChanged; + UpdateStatus(); + } + + private void OnWidthChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (BarContainer == null) + return; + var width = (int)WidthSlider.Value; + BarContainer.Width = width; + WidthLabel.Text = $"{width}"; + } + + private void OnDynamicOverflowChanged(object? sender, RoutedEventArgs e) + { + if (DemoBar == null) + return; + DemoBar.IsDynamicOverflowEnabled = DynamicOverflowCheck.IsChecked == true; + } + + private void OnOverflowChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateStatus(); + } + + private void UpdateStatus() + { + var total = DemoBar.PrimaryCommands.Count; + var overflow = DemoBar.OverflowItems.Count; + var visible = total - overflow; + StatusText.Text = $"Showing {visible} of {total} commands, {overflow} in overflow"; + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml new file mode 100644 index 0000000000..b83d1c3e57 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml @@ -0,0 +1,94 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z + M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z + M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z + M18,3H6V7H18M19,12A1,1 0 0,1 18,11A1,1 0 0,1 19,10A1,1 0 0,1 20,11A1,1 0 0,1 19,12M16,19H8V14H16M19,8H5A3,3 0 0,0 2,11V17H6V21H18V17H22V11A3,3 0 0,0 19,8Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A toolbar supporting primary and secondary commands with an optional overflow menu. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml.cs new file mode 100644 index 0000000000..c624ffcad6 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarFirstLookPage.xaml.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarFirstLookPage : UserControl + { + public CommandBarFirstLookPage() + { + InitializeComponent(); + } + + private void OnButtonClick(object? sender, RoutedEventArgs e) + { + if (sender is AppBarButton btn) + StatusText.Text = $"{btn.Label} clicked"; + } + + private void OnToggleChanged(object? sender, RoutedEventArgs e) + { + if (sender is AppBarToggleButton btn) + StatusText.Text = btn.IsChecked == true + ? $"{btn.Label} enabled" + : $"{btn.Label} disabled"; + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml new file mode 100644 index 0000000000..a1dbea2db1 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml @@ -0,0 +1,47 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + The DefaultLabelPosition property controls how button labels are displayed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml.cs new file mode 100644 index 0000000000..db9cfa3e82 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarLabelPositionPage.xaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarLabelPositionPage : UserControl + { + public CommandBarLabelPositionPage() + { + InitializeComponent(); + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarOverflowPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarOverflowPage.xaml new file mode 100644 index 0000000000..be3371a8e8 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarOverflowPage.xaml @@ -0,0 +1,74 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs new file mode 100644 index 0000000000..6c771f3078 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs @@ -0,0 +1,464 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ControlsGalleryAppPage : UserControl +{ + static readonly Color Accent = Color.Parse("#60CDFF"); + static readonly Color ContentBg = Color.Parse("#141414"); + static readonly Color CardBg = Color.Parse("#1F1F1F"); + static readonly Color BorderCol = Color.Parse("#2EFFFFFF"); + static readonly Color TextCol = Color.Parse("#FFFFFF"); + static readonly Color TextSec = Color.Parse("#C8FFFFFF"); + static readonly Color TextMuted = Color.Parse("#80FFFFFF"); + + DrawerPage? _drawer; + NavigationPage? _detailNav; + Button? _selectedBtn; + TextBox? _searchBox; + ContentPage? _preSearchPage; + bool _isSearching; + + public ControlsGalleryAppPage() + { + InitializeComponent(); + + _drawer = this.FindControl("NavDrawer"); + _detailNav = this.FindControl("DetailNav"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs new file mode 100644 index 0000000000..b1d84fb5fd --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCompactPage : UserControl + { + private bool _isLoaded; + + public DrawerPageCompactPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.Opened += OnDrawerStatusChanged; + DemoDrawer.Closed += OnDrawerStatusChanged; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerStatusChanged; + DemoDrawer.Closed -= OnDrawerStatusChanged; + } + + private void OnDrawerStatusChanged(object? sender, System.EventArgs e) => UpdateStatus(); + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.CompactOverlay, + 1 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.CompactOverlay + }; + } + + private void OnCompactLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.CompactDrawerLength = e.NewValue; + CompactLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + StatusText.Text = $"Drawer: {(DemoDrawer.IsOpen ? "Open" : "Closed")}"; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml new file mode 100644 index 0000000000..11f584c39d --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs new file mode 100644 index 0000000000..c05d92ecf6 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs @@ -0,0 +1,146 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomFlyoutPage : UserControl + { + private Ellipse? _bubble1; + private Ellipse? _bubble2; + private DispatcherTimer? _bubbleTimer; + private double _bubblePhase; + + public DrawerPageCustomFlyoutPage() + { + InitializeComponent(); + + _bubble1 = this.FindControl("Bubble1"); + _bubble2 = this.FindControl("Bubble2"); + + DrawerPageControl.PropertyChanged += (_, args) => + { + if (args.Property == DrawerPage.IsOpenProperty) + OnDrawerOpenChanged((bool)args.NewValue!); + }; + + _ = DetailNav.PushAsync(BuildDetailPage("Home"), null); + } + + private Control[] MenuItems => + new Control[] { MenuItem1, MenuItem2, MenuItem3, MenuItem4, MenuItem5, FooterRow }; + + private void OnDrawerOpenChanged(bool isOpen) + { + if (isOpen) + { + StartBubbles(); + + foreach (var item in MenuItems) + { + item.Opacity = 1.0; + if (item.RenderTransform is TranslateTransform tt) + tt.Y = 0; + } + } + else + { + StopBubbles(); + + foreach (var item in MenuItems) + { + var savedItemT = item.Transitions; + item.Transitions = null; + item.Opacity = 0.0; + item.Transitions = savedItemT; + + if (item.RenderTransform is TranslateTransform tt) + { + var savedTT = tt.Transitions; + tt.Transitions = null; + tt.Y = 25; + tt.Transitions = savedTT; + } + } + } + } + + private void StartBubbles() + { + if (_bubbleTimer != null) return; + _bubblePhase = 0; + _bubbleTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + _bubbleTimer.Tick += OnBubbleTick; + _bubbleTimer.Start(); + } + + private void StopBubbles() + { + if (_bubbleTimer == null) return; + _bubbleTimer.Stop(); + _bubbleTimer.Tick -= OnBubbleTick; + _bubbleTimer = null; + + if (_bubble1 != null) _bubble1.RenderTransform = null; + if (_bubble2 != null) _bubble2.RenderTransform = null; + } + + private void OnBubbleTick(object? sender, EventArgs e) + { + _bubblePhase += 0.012; + + if (_bubble1 != null) + _bubble1.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.65) * 10, + y: Math.Sin(_bubblePhase) * 14); + + if (_bubble2 != null) + _bubble2.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.45 + 1.8) * 7, + y: Math.Cos(_bubblePhase * 0.85 + 0.6) * 10); + } + + private async void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var tag = button.Tag?.ToString() ?? "Home"; + + DrawerPageControl.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildDetailPage(tag), null); + } + + private static ContentPage BuildDetailPage(string section) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Welcome back! Here is your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml new file mode 100644 index 0000000000..cba7837314 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs new file mode 100644 index 0000000000..697e67f0f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs @@ -0,0 +1,192 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomizationPage : UserControl + { + private bool _isLoaded; + + private static readonly string[] _iconPaths = + { + // 0 - 3 lines (default hamburger) + "M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6 18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z", + // 1 - 2 lines + "M3,13H21V11H3M3,6V8H21V6", + // 2 - 4 squares + "M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3", + }; + + public DrawerPageCustomizationPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnBehaviorChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBehavior = BehaviorCombo.SelectedIndex switch + { + 0 => DrawerBehavior.Auto, + 1 => DrawerBehavior.Flyout, + 2 => DrawerBehavior.Locked, + 3 => DrawerBehavior.Disabled, + _ => DrawerBehavior.Auto + }; + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Overlay, + 1 => DrawerLayoutBehavior.Split, + 2 => DrawerLayoutBehavior.CompactOverlay, + 3 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.Overlay + }; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 1 => DrawerPlacement.Right, + 2 => DrawerPlacement.Top, + 3 => DrawerPlacement.Bottom, + _ => DrawerPlacement.Left + }; + } + + private void OnGestureToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is CheckBox check) + DemoDrawer.IsGestureEnabled = check.IsChecked == true; + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBackground = DrawerBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.SlateBlue), + 2 => new SolidColorBrush(Colors.DarkCyan), + 3 => new SolidColorBrush(Colors.DarkRed), + 4 => new SolidColorBrush(Colors.DarkGreen), + _ => null + }; + } + + private void OnHeaderBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerHeaderBackground = HeaderBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DodgerBlue), + 2 => new SolidColorBrush(Colors.Orange), + 3 => new SolidColorBrush(Colors.Teal), + 4 => new SolidColorBrush(Colors.Purple), + _ => null + }; + } + + private void OnFooterBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerFooterBackground = FooterBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DimGray), + 2 => new SolidColorBrush(Colors.DarkSlateBlue), + 3 => new SolidColorBrush(Colors.DarkOliveGreen), + 4 => new SolidColorBrush(Colors.Maroon), + _ => null + }; + } + + private void OnIconChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerIcon = Geometry.Parse(_iconPaths[IconCombo.SelectedIndex]); + } + + private void OnBackdropChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.BackdropBrush = BackdropCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)), + 2 => new SolidColorBrush(Color.FromArgb(179, 0, 0, 0)), + 3 => new SolidColorBrush(Color.FromArgb(102, 255, 255, 255)), + _ => null + }; + } + + private void OnShowHeaderToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowHeaderCheck.IsChecked == true) + DemoDrawer.DrawerHeader = DrawerHeaderBorder; + else + DemoDrawer.DrawerHeader = null; + } + + private void OnShowFooterToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowFooterCheck.IsChecked == true) + DemoDrawer.DrawerFooter = DrawerFooterBorder; + else + DemoDrawer.DrawerFooter = null; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + + if (DemoDrawer.DrawerBehavior != DrawerBehavior.Locked) + DemoDrawer.IsOpen = false; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml new file mode 100644 index 0000000000..92a2055812 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs new file mode 100644 index 0000000000..02148dc46b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageEventsPage : UserControl + { + private readonly Dictionary _sectionPages; + + public DrawerPageEventsPage() + { + InitializeComponent(); + + _sectionPages = new Dictionary + { + ["Home"] = CreateSectionPage("Home"), + ["Profile"] = CreateSectionPage("Profile"), + ["Settings"] = CreateSectionPage("Settings"), + }; + + foreach (var (name, page) in _sectionPages) + { + var label = name; + page.NavigatedTo += (_, _) => Log($"{label}: NavigatedTo"); + page.NavigatedFrom += (_, _) => Log($"{label}: NavigatedFrom"); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + DemoDrawer.Opened += OnDrawerOpened; + DemoDrawer.Closing += OnClosing; + DemoDrawer.Closed += OnDrawerClosed; + // Set Content here so the initial NavigatedTo events fire + // (VisualRoot is null in the constructor, which suppresses lifecycle events). + DemoDrawer.Content = _sectionPages["Home"]; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerOpened; + DemoDrawer.Closing -= OnClosing; + DemoDrawer.Closed -= OnDrawerClosed; + } + + private void OnDrawerOpened(object? sender, System.EventArgs e) => Log("Opened"); + private void OnDrawerClosed(object? sender, System.EventArgs e) => Log("Closed"); + + private void OnToggle(object? sender, RoutedEventArgs e) + { + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnClosing(object? sender, DrawerClosingEventArgs e) + { + if (CancelCheck.IsChecked == true) + { + e.Cancel = true; + CancelCheck.IsChecked = false; + Log("Closing \u2192 cancelled"); + } + else + { + Log("Closing"); + } + } + + private void OnSelectSection(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + var section = btn.Tag?.ToString() ?? "Home"; + + if (!_sectionPages.TryGetValue(section, out var page)) return; + if (ReferenceEquals(DemoDrawer.Content, page)) + { + DemoDrawer.IsOpen = false; + return; + } + + Log($"\u2192 {section}"); + DemoDrawer.Content = page; + DemoDrawer.IsOpen = false; + } + + private void OnClearLog(object? sender, RoutedEventArgs e) + { + EventLog.Text = string.Empty; + } + + private void Log(string message) + { + EventLog.Text = $"{message}\n{EventLog.Text}"; + } + + private static ContentPage CreateSectionPage(string header) => new ContentPage + { + Header = header, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 24, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + }, + new TextBlock + { + Text = "Tap a drawer item to navigate.\nWatch the event log in the panel.", + TextWrapping = TextWrapping.Wrap, + Opacity = 0.6, + TextAlignment = TextAlignment.Center, + FontSize = 13, + } + } + } + }; + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml new file mode 100644 index 0000000000..e257137ed9 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs new file mode 100644 index 0000000000..36454ef8d3 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageRtlPage : UserControl + { + public DrawerPageRtlPage() + { + InitializeComponent(); + } + + private void OnRtlToggled(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.FlowDirection = RtlCheckBox.IsChecked == true + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 0 => DrawerPlacement.Left, + 1 => DrawerPlacement.Right, + _ => DrawerPlacement.Left + }; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + DetailDescriptionText.Text = item switch + { + "Home" => "Toggle RTL to see the drawer flip to the right edge.\nGestures are mirrored: drag from right edge to open, drag right to close.", + "Profile" => "View and edit your profile information here.", + "Messages" => "Your messages and notifications appear here.", + "Settings" => "Configure application preferences and options.", + _ => $"Content for {item}" + }; + + DemoDrawer.IsOpen = false; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml new file mode 100644 index 0000000000..9776163b6a --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs new file mode 100644 index 0000000000..23634c1707 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageTransitionsPage : UserControl + { + private string _selectedTransition = "None"; + + public DrawerPageTransitionsPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + // Null out the default transition — OnTransitionChanged runs during init before DetailNav exists. + DetailNav.PageTransition = null; + await DetailNav.PushAsync(BuildPage("Home", _selectedTransition), null); + } + + private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e) + { + if (DetailNav == null) return; + + _selectedTransition = TransitionCombo.SelectedIndex switch + { + 1 => "CrossFade", + 2 => "PageSlide (H)", + 3 => "PageSlide (V)", + 4 => "Composite (Slide + Fade)", + _ => "None" + }; + + DetailNav.PageTransition = TransitionCombo.SelectedIndex switch + { + 1 => new CrossFade(TimeSpan.FromMilliseconds(300)), + 2 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + 3 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical), + 4 => new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + new CrossFade(TimeSpan.FromMilliseconds(300)) + } + }, + _ => null + }; + } + + private async void OnSectionClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var section = button.Tag?.ToString() ?? "Home"; + + DemoDrawer.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildPage(section, _selectedTransition)); + } + + private static ContentPage BuildPage(string section, string transitionName) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0, $"Transition: {transitionName}"); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml new file mode 100644 index 0000000000..22320fbc8d --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs new file mode 100644 index 0000000000..49b13b4d90 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs @@ -0,0 +1,319 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#2E7D32"); + static readonly Color Accent = Color.Parse("#4CAF50"); + static readonly Color BgLight = Color.Parse("#F1F8E9"); + static readonly Color TextDark = Color.Parse("#1A2E1C"); + static readonly Color TextMuted = Color.Parse("#90A4AE"); + + const string LeafPath = + "M12 3C9 6 6 9 6 13C6 17.4 8.7 21 12 22C15.3 21 18 17.4 18 13C18 9 15 6 12 3Z"; + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + Button? _selectedBtn; + + public EcoTrackerAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs new file mode 100644 index 0000000000..3f36a4d48b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerHomeView : UserControl +{ + public Action? TreeDetailRequested { get; set; } + + public EcoTrackerHomeView() => InitializeComponent(); + + void OnHeroClick(object? sender, RoutedEventArgs e) => TreeDetailRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml new file mode 100644 index 0000000000..82a5e938df --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs new file mode 100644 index 0000000000..90aa02066b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerStatsView : UserControl +{ + public EcoTrackerStatsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml new file mode 100644 index 0000000000..77a0e3299c --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs new file mode 100644 index 0000000000..f0d4d7d622 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs @@ -0,0 +1,118 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ModernAppPage : UserControl +{ + // Palette + static readonly Color Primary = Color.Parse("#0dccf2"); + static readonly Color BgLight = Color.Parse("#f5f8f8"); + + static IBrush BgBrush => new SolidColorBrush(BgLight); + + DrawerPage? _drawerPage; + NavigationPage? _navPage; + ScrollViewer? _infoPanel; + TextBlock? _pageTitle; + Button? _selectedNavBtn; + + public ModernAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _drawerPage = this.FindControl("DrawerPageControl"); + _navPage = this.FindControl("NavPage"); + _pageTitle = this.FindControl("PageTitle"); + + if (_navPage != null) + NavigateToDiscover(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 640; + } + + void OnNavClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + SelectNavButton(btn); + _drawerPage!.IsOpen = false; + switch (btn.Tag?.ToString()) + { + case "Discover": NavigateToDiscover(); break; + case "MyTrips": NavigateToMyTrips(); break; + case "Profile": NavigateToProfile(); break; + case "Settings": NavigateToSettings(); break; + } + } + + void OnCloseDrawer(object? sender, RoutedEventArgs e) + { + if (_drawerPage != null) _drawerPage.IsOpen = false; + } + + void SelectNavButton(Button btn) + { + if (_selectedNavBtn != null) + _selectedNavBtn.Background = Brushes.Transparent; + _selectedNavBtn = btn; + btn.Background = new SolidColorBrush(Color.Parse("#1A0dccf2")); + } + + async Task Navigate(ContentPage page) + { + if (_navPage == null) return; + NavigationPage.SetHasBackButton(page, false); + NavigationPage.SetHasNavigationBar(page, false); + await _navPage.PopToRootAsync(); + await _navPage.PushAsync(page); + } + + async void NavigateToDiscover() + { + if (_pageTitle != null) _pageTitle.Text = "Discover"; + SelectNavButton(this.FindControl + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs new file mode 100644 index 0000000000..ae2cdb9451 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernProfileView : UserControl +{ + public ModernProfileView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml new file mode 100644 index 0000000000..a4ee8cd8bb --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs new file mode 100644 index 0000000000..d50ade3dda --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernSettingsView : UserControl +{ + public ModernSettingsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index c480b512b4..2906091daa 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -52,7 +52,7 @@ namespace ControlCatalog.Pages } }; - RotationGesture.AddHandler(Gestures.PinchEvent, (s, e) => + RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) => { AngleSlider.Value = e.Angle; }); @@ -94,7 +94,7 @@ namespace ControlCatalog.Pages } }; - control.AddHandler(Gestures.PinchEvent, (s, e) => + control.AddHandler(InputElement.PinchEvent, (s, e) => { InitComposition(control!); @@ -114,7 +114,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.PinchEndedEvent, (s, e) => + control.AddHandler(InputElement.PinchEndedEvent, (s, e) => { InitComposition(control!); @@ -124,7 +124,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.ScrollGestureEvent, (s, e) => + control.AddHandler(InputElement.ScrollGestureEvent, (s, e) => { InitComposition(control!); @@ -134,8 +134,8 @@ namespace ControlCatalog.Pages var currentSize = control.Bounds.Size * _currentScale; - currentOffset = new Vector3D(MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), - (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), + currentOffset = new Vector3D(Math.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), + (float)Math.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), 0); compositionVisual.Offset = currentOffset * -1; @@ -171,7 +171,7 @@ namespace ControlCatalog.Pages } }; - control.AddHandler(Gestures.PullGestureEvent, (s, e) => + control.AddHandler(InputElement.PullGestureEvent, (s, e) => { Vector3D center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0); InitComposition(ball!); @@ -183,7 +183,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.PullGestureEndedEvent, (s, e) => + control.AddHandler(InputElement.PullGestureEndedEvent, (s, e) => { InitComposition(ball!); if (ballCompositionVisual != null) diff --git a/samples/ControlCatalog/Pages/NavigationDemoHelper.cs b/samples/ControlCatalog/Pages/NavigationDemoHelper.cs new file mode 100644 index 0000000000..22e52b6fda --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoHelper.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for ControlCatalog demo pages. + /// + internal static class NavigationDemoHelper + { + /// + /// Pastel background brushes cycled by page index. + /// + internal static readonly IBrush[] PageBrushes = + { + new SolidColorBrush(Color.Parse("#BBDEFB")), + new SolidColorBrush(Color.Parse("#C8E6C9")), + new SolidColorBrush(Color.Parse("#FFE0B2")), + new SolidColorBrush(Color.Parse("#E1BEE7")), + new SolidColorBrush(Color.Parse("#FFCDD2")), + new SolidColorBrush(Color.Parse("#B2EBF2")), + }; + + internal static IBrush GetPageBrush(int index) => + PageBrushes[((index % PageBrushes.Length) + PageBrushes.Length) % PageBrushes.Length]; + + /// + /// Creates a simple demo ContentPage with a centered title and subtitle. + /// + internal static ContentPage MakePage(string header, string body, int colorIndex) => + new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = body, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 260 + } + } + }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + /// + /// Creates a demo ContentPage with an icon, title, body, and hint text + /// (used by DrawerPage detail pages). + /// + internal static ContentPage MakeSectionPage( + string header, string iconData, string title, string body, + int colorIndex, string? hint = null) + { + var panel = new StackPanel { Margin = new Thickness(24, 20), Spacing = 12 }; + + panel.Children.Add(new PathIcon + { + Width = 48, + Height = 48, + Data = Geometry.Parse(iconData), + Foreground = new SolidColorBrush(Color.Parse("#0078D4")) + }); + panel.Children.Add(new TextBlock + { + Text = title, + FontSize = 26, + FontWeight = FontWeight.Bold + }); + panel.Children.Add(new TextBlock + { + Text = body, + FontSize = 14, + Opacity = 0.8, + TextWrapping = TextWrapping.Wrap + }); + panel.Children.Add(new Separator { Margin = new Thickness(0, 8) }); + + if (hint != null) + { + panel.Children.Add(new TextBlock + { + Text = hint, + FontSize = 12, + Opacity = 0.45, + FontStyle = FontStyle.Italic, + TextWrapping = TextWrapping.Wrap + }); + } + + return new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new ScrollViewer { Content = panel }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + } + + private static readonly Geometry CloseIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + + /// + /// Builds the demo gallery home page for NavigationPage/TabbedPage/DrawerPage demo registries. + /// + internal static ContentPage CreateGalleryHomePage( + NavigationPage nav, + (string Group, string Title, string Description, Func Factory)[] demos) + { + var stack = new StackPanel { Margin = new Thickness(12), Spacing = 16 }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 170, + MinHeight = 80, + Margin = new Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (_, _) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon { Data = CloseIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await nav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(homePage, false); + return homePage; + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml new file mode 100644 index 0000000000..4849b2d5b8 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs new file mode 100644 index 0000000000..b4ec1503bd --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs @@ -0,0 +1,91 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class NavigationDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic NavigationPage with push/pop navigation and back button support.", + () => new NavigationPageFirstLookPage()), + ("Overview", "Modal Navigation", "Push and pop modal pages that appear on top of the navigation stack.", + () => new NavigationPageModalPage()), + ("Overview", "Navigation Events", + "Subscribe to Pushed, Popped, PoppedToRoot, ModalPushed, and ModalPopped events.", + () => new NavigationPageEventsPage()), + + // Appearance + ("Appearance", "Bar Customization", + "Customize the navigation bar background, foreground, shadow, and visibility.", + () => new NavigationPageAppearancePage()), + ("Appearance", "Header", + "Set page header content: a string, icon, or any custom control in the navigation bar.", + () => new NavigationPageTitlePage()), + + // Data + ("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.", + () => new NavigationPagePassDataPage()), + + // Features + ("Features", "Attached Methods", + "Per-page navigation bar and back button control via static attached methods.", + () => new NavigationPageAttachedMethodsPage()), + ("Features", "Back Button", "Customize, hide, or intercept the back button.", + () => new NavigationPageBackButtonPage()), + ("Features", "CommandBar", + "Add, remove and position CommandBar items inside the navigation bar or as a bottom bar.", + () => new NavigationPageToolbarPage()), + ("Features", "Transitions", + "Configure page transitions: PageSlide, Parallax Slide, CrossFade, Fade Through, and more.", + () => new NavigationPageTransitionsPage()), + ("Features", "Modal Transitions", "Configure modal transition: PageSlide from bottom, CrossFade, or None.", + () => new NavigationPageModalTransitionsPage()), + ("Features", "Stack Management", "Remove or insert pages anywhere in the navigation stack at runtime.", + () => new NavigationPageStackPage()), + ("Features", "Interactive Header", + "Build a header with a title and live search box that filters page content in real time.", + () => new NavigationPageInteractiveHeaderPage()), + ("Features", "Back Swipe Gesture", "Swipe from the left edge to interactively pop the current page.", + () => new NavigationPageGesturePage()), + ("Features", "Scroll-Aware Bar", + "Hide the navigation bar on downward scroll and reveal it on upward scroll.", + () => new NavigationPageScrollAwarePage()), + + // Performance + ("Performance", "Performance Monitor", + "Track stack depth, live page instances, and managed heap size. Observe how memory is reclaimed after popping pages.", + () => new NavigationPagePerformancePage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Login flow with RemovePage, TabbedPage dashboard with bottom tabs, and NavigationPage push for workout detail.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir", + "Restaurant app with DrawerPage flyout menu, TabbedPage bottom tabs, and NavigationPage push for dish detail.", + () => new LAvenirAppPage()), + ("Showcases", "AvaloniaFlix", + "Streaming app with dark NavigationPage, hidden nav bar on home, and custom bar tint on movie detail pages.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + ("Showcases", "Curved Header", + "Shop app with dome-bottomed white header on home (nav bar hidden) and blue curved header on detail (BarLayoutBehavior.Overlay).", + () => new NavigationPageCurvedHeaderPage()), + }; + + public NavigationDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml new file mode 100644 index 0000000000..6e209c8bb5 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs new file mode 100644 index 0000000000..a2ae34dd1a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixAppPage : UserControl +{ + NavigationPage? _detailNav; + ScrollViewer? _infoPanel; + + public AvaloniaFlixAppPage() + { + InitializeComponent(); + + _detailNav = this.FindControl("DetailNav"); + if (_detailNav != null) + { + _detailNav.ModalTransition = new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical); + + var homeView = new AvaloniaFlixHomeView(); + homeView.MovieSelected = title => PushDetailPage(title); + homeView.SearchRequested = () => _ = PushSearchPageAsync(); + + var homePage = new ContentPage + { + Content = homeView, + Background = Brushes.Transparent, + Header = BuildHomeHeader(), + }; + NavigationPage.SetTopCommandBar(homePage, BuildHomeCommandBar()); + _ = _detailNav.PushAsync(homePage); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TextBlock BuildHomeHeader() => new TextBlock + { + Text = "AVALONIAFLIX", + Foreground = new SolidColorBrush(Color.Parse("#E50914")), + FontSize = 18, + FontWeight = Avalonia.Media.FontWeight.Black, + VerticalAlignment = VerticalAlignment.Center, + }; + + StackPanel BuildHomeCommandBar() + { + var cmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var searchBtn = new Button { Padding = new Thickness(4) }; + searchBtn.Classes.Add("flixTransparent"); + searchBtn.Click += OnSearchClick; + searchBtn.Content = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + cmdBar.Children.Add(searchBtn); + cmdBar.Children.Add(new Border + { + Width = 30, Height = 30, CornerRadius = new CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#333333")), + Child = new TextBlock + { + Text = "JD", FontSize = 10, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + }); + return cmdBar; + } + + async void PushDetailPage(string title) + { + if (_detailNav == null) return; + + var detailView = new AvaloniaFlixDetailView(title); + + var headerTitle = new TextBlock + { + Text = title, FontSize = 17, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, + }; + + var shareBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + var shareBtn = new Button { Padding = new Thickness(8), Content = shareBtnContent }; + shareBtn.Classes.Add("flixTransparent"); + + var bookmarkBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z"), + }; + var bookmarkBtn = new Button { Padding = new Thickness(8), Content = bookmarkBtnContent }; + bookmarkBtn.Classes.Add("flixTransparent"); + + var detailCmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, Spacing = 8, + VerticalAlignment = VerticalAlignment.Center, + }; + detailCmdBar.Children.Add(shareBtn); + detailCmdBar.Children.Add(bookmarkBtn); + + var detailPage = new ContentPage + { + Content = detailView, + Background = Brushes.Transparent, + Header = headerTitle, + }; + NavigationPage.SetTopCommandBar(detailPage, detailCmdBar); + + await _detailNav.PushAsync(detailPage); + + var drawer = this.FindControl("DrawerPageControl"); + if (drawer is { IsOpen: true }) + drawer.IsOpen = false; + } + + async void OnSearchClick(object? sender, RoutedEventArgs e) + { + await PushSearchPageAsync(); + } + + async Task PushSearchPageAsync() + { + if (_detailNav == null) return; + + var searchView = new AvaloniaFlixSearchView(); + searchView.CloseRequested = async () => await (_detailNav?.PopModalAsync() ?? Task.CompletedTask); + searchView.MovieSelected = async title => + { + if (_detailNav != null && _detailNav.ModalStack.Count > 0) + await _detailNav.PopModalAsync(); + PushDetailPage(title); + }; + + var searchPage = new ContentPage + { + Content = searchView, + Background = new SolidColorBrush(Color.Parse("#0A0A0A")), + }; + NavigationPage.SetHasNavigationBar(searchPage, false); + + await (_detailNav?.PushModalAsync(searchPage) ?? Task.CompletedTask); + } + + void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + var drawer = this.FindControl("DrawerPageControl"); + if (drawer != null) + drawer.IsOpen = false; + + _ = _detailNav?.PopToRootAsync(); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml new file mode 100644 index 0000000000..e6f530669c --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs new file mode 100644 index 0000000000..659fea52e0 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixDetailView : UserControl +{ + static readonly string[] MovieAssets = + { + "avares://ControlCatalog/Assets/Movies/trending1.jpg", + "avares://ControlCatalog/Assets/Movies/trending2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated1.jpg", + "avares://ControlCatalog/Assets/Movies/toprated2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated3.jpg", + "avares://ControlCatalog/Assets/Movies/toprated4.jpg", + "avares://ControlCatalog/Assets/Movies/continue1.jpg", + "avares://ControlCatalog/Assets/Movies/morelike1.jpg", + "avares://ControlCatalog/Assets/Movies/search1.jpg", + "avares://ControlCatalog/Assets/Movies/hero.jpg", + "avares://ControlCatalog/Assets/Movies/cast1.jpg", + "avares://ControlCatalog/Assets/Movies/cast2.jpg", + }; + + public AvaloniaFlixDetailView() => InitializeComponent(); + + public AvaloniaFlixDetailView(string movieTitle) + { + InitializeComponent(); + + HeroTitleLabel.Text = movieTitle; + + var rng = new Random(movieTitle.GetHashCode()); + int imgIdx = Math.Abs(movieTitle.GetHashCode()) % MovieAssets.Length; + + string year = (2020 + rng.Next(6)).ToString(); + string rating = $"{6.5 + rng.NextDouble() * 3.0:F1}/10"; + int mins = 90 + rng.Next(60); + string duration = $"{mins / 60}h {mins % 60}m"; + + YearLabel.Text = year; + RatingLabel.Text = rating; + DurationLabel.Text = duration; + + try + { + var uri = new Uri(MovieAssets[imgIdx]); + HeroBg.Background = new ImageBrush(new Bitmap(AssetLoader.Open(uri))) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + HeroBg.Background = new SolidColorBrush(Color.Parse("#111111")); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml new file mode 100644 index 0000000000..4f186e910f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs new file mode 100644 index 0000000000..81702bf695 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixHomeView : UserControl +{ + public Action? MovieSelected { get; set; } + public Action? SearchRequested { get; set; } + + public AvaloniaFlixHomeView() => InitializeComponent(); + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Cyber Dune"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml new file mode 100644 index 0000000000..d4c536521f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs new file mode 100644 index 0000000000..aac3069bed --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? MovieSelected { get; set; } + + public AvaloniaFlixSearchView() => InitializeComponent(); + + void OnCloseClick(object? sender, RoutedEventArgs e) + { + if (CloseRequested != null) + { + CloseRequested(); + } + else + { + var nav = this.FindAncestorOfType(); + _ = nav?.PopModalAsync() ?? Task.CompletedTask; + } + } + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Neon Horizon"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml new file mode 100644 index 0000000000..8810c5777b --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs new file mode 100644 index 0000000000..c8910584bc --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class CurvedHeaderHomeScrollView : UserControl +{ + public Action? NavigateRequested { get; set; } + + public CurvedHeaderHomeScrollView() => InitializeComponent(); + + void OnShopNowClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); + + void OnProductClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml new file mode 100644 index 0000000000..ecfeb1bb78 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs new file mode 100644 index 0000000000..6c4a67a473 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages; + +public partial class LAvenirAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#4b2bee"); + static readonly Color BgDark = Color.Parse("#131022"); + static readonly Color BgLight = Color.Parse("#f6f6f8"); + static readonly Color TextDark = Color.Parse("#1e293b"); + static readonly Color TextMuted = Color.Parse("#94a3b8"); + static readonly Color BorderLight = Color.Parse("#e2e8f0"); + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + + public LAvenirAppPage() + { + InitializeComponent(); + + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + + if (_navPage != null) + _ = _navPage.PushAsync(BuildMenuTabbedPage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TabbedPage BuildMenuTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgLight), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(200)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = Brushes.White; + tp.Resources["TabbedPageTabStripBorderThickness"] = new Thickness(0, 1, 0, 0); + tp.Resources["TabbedPageTabStripBorderBrush"] = new SolidColorBrush(BorderLight); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Primary); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(TextMuted); + + tp.IndicatorTemplate = new FuncDataTemplate((_, _) => + new Ellipse + { + Width = 5, Height = 5, + Margin = new Thickness(0, 10, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + Fill = new SolidColorBrush(Primary), + }); + + tp.Header = new TextBlock + { + Text = "L'Avenir", + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(TextDark), + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + }; + + NavigationPage.SetTopCommandBar(tp, new Button + { + Width = 40, + Height = 40, + CornerRadius = new CornerRadius(12), + Background = Brushes.Transparent, + Foreground = new SolidColorBrush(TextDark), + Padding = new Thickness(8), + BorderThickness = new Thickness(0), + Content = new PathIcon + { + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + Width = 18, + Height = 18, + }, + VerticalAlignment = VerticalAlignment.Center, + }); + + var menuView = new LAvenirMenuView(); + menuView.DishSelected = PushDishDetail; + + var menuPage = new ContentPage + { + Content = menuView, + Background = new SolidColorBrush(BgLight), + Header = "Menu", + Icon = "M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z", + }; + + var reservationsPage = new ContentPage + { + Content = new LAvenirReservationsView(), + Background = new SolidColorBrush(BgLight), + Header = "Reservations", + Icon = "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z", + }; + + var profilePage = new ContentPage + { + Content = new LAvenirProfileView(), + Background = new SolidColorBrush(BgLight), + Header = "Profile", + Icon = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", + }; + + tp.Pages = new ObservableCollection { menuPage, reservationsPage, profilePage }; + return tp; + } + + async void PushDishDetail(string name, string price, string description, string imageFile) + { + if (_navPage == null) return; + + var detail = new ContentPage + { + Content = new LAvenirDishDetailView(name, price, description, imageFile), + Background = new SolidColorBrush(BgDark), + Header = name, + }; + NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price)); + + _navPage.Background = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarForeground"] = Brushes.White; + + detail.NavigatedFrom += (_, _) => + { + if (_navPage != null) + { + _navPage.Background = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); + } + }; + + await _navPage.PushAsync(detail); + } + + Border BuildFloatingBar(string price) + { + var bar = new Border + { + CornerRadius = new CornerRadius(16), + Background = new SolidColorBrush(Color.FromArgb(178, BgDark.R, BgDark.G, BgDark.B)), + BorderBrush = new SolidColorBrush(Color.FromArgb(51, 255, 255, 255)), + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Margin = new Thickness(16, 8, 16, 8), + }; + + var barGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + + var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + info.Children.Add(new TextBlock + { + Text = "Add to Order", + FontSize = 14, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + }); + info.Children.Add(new TextBlock + { + Text = price, + FontSize = 12, + FontWeight = FontWeight.Medium, + Foreground = new SolidColorBrush(TextMuted), + }); + barGrid.Children.Add(info); + + var addBtn = new Button + { + Content = "Add", + Width = 80, + Height = 40, + CornerRadius = new CornerRadius(10), + Background = new SolidColorBrush(Primary), + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + FontSize = 14, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + }; + var hoverStyle = new Style(x => x.OfType + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs new file mode 100644 index 0000000000..5ea6d72cf8 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class LAvenirReservationsView : UserControl +{ + public LAvenirReservationsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml new file mode 100644 index 0000000000..f519b418d7 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs new file mode 100644 index 0000000000..5f98cee05f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseHomeView : UserControl +{ + public Action? WorkoutDetailRequested { get; set; } + + public PulseHomeView() => InitializeComponent(); + + void OnRecCard1Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard2Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard3Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnPlayButtonClicked(object? sender, RoutedEventArgs e) => + WorkoutDetailRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml b/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml new file mode 100644 index 0000000000..2309090409 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs new file mode 100644 index 0000000000..7999d3d709 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutDetailView : UserControl +{ + public Action? BackRequested { get; set; } + + public PulseWorkoutDetailView() => InitializeComponent(); + + void OnBackClicked(object? sender, RoutedEventArgs e) => + BackRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml new file mode 100644 index 0000000000..c1f9659b05 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs new file mode 100644 index 0000000000..fec95be3f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutsView : UserControl +{ + public PulseWorkoutsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml new file mode 100644 index 0000000000..7cd254b415 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs new file mode 100644 index 0000000000..46951950b7 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingAppPage : UserControl +{ + static readonly Color BgColor = Color.Parse("#120a1f"); + static readonly Color SurfaceColor = Color.Parse("#2d1b4e"); + static readonly Color CyanColor = Color.Parse("#00ffff"); + static readonly Color YellowColor = Color.Parse("#ffff00"); + static readonly Color MutedColor = Color.Parse("#7856a8"); + static readonly Color TextColor = Color.Parse("#e0d0ff"); + + NavigationPage? _nav; + ScrollViewer? _infoPanel; + + public RetroGamingAppPage() + { + InitializeComponent(); + + _nav = this.FindControl("RetroNav"); + if (_nav != null) + _ = _nav.PushAsync(BuildHomePage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + ContentPage BuildHomePage() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + page.Header = BuildPixelArcadeLogo(); + NavigationPage.SetTopCommandBar(page, BuildNavBarRight()); + + var panel = new Panel(); + panel.Children.Add(BuildHomeTabbedPage()); + panel.Children.Add(BuildSearchFab()); + + page.Content = panel; + return page; + } + + static Control BuildPixelArcadeLogo() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + + var iconPanel = new Grid { Width = 36, Height = 30 }; + iconPanel.Children.Add(new Border + { + Width = 36, Height = 20, CornerRadius = new CornerRadius(3), + Background = new SolidColorBrush(Color.Parse("#cc44dd")), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(4, 0, 0, 6), + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 4, 6), + }); + row.Children.Add(iconPanel); + + var textStack = new StackPanel { Spacing = 1 }; + textStack.Children.Add(new TextBlock + { + Text = "PIXEL", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + textStack.Children.Add(new TextBlock + { + Text = "ARCADE", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + row.Children.Add(textStack); + return row; + } + + static Control BuildNavBarRight() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + row.Children.Add(new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"), + }); + var avatar = new Border + { + Width = 26, Height = 26, + CornerRadius = new CornerRadius(0), + ClipToBounds = true, + Background = new SolidColorBrush(SurfaceColor), + BorderBrush = new SolidColorBrush(MutedColor), + BorderThickness = new Thickness(1), + }; + avatar.Child = new TextBlock + { + Text = "P1", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 7, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(CyanColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + row.Children.Add(avatar); + return row; + } + + TabbedPage BuildHomeTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgColor), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(250)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = new SolidColorBrush(SurfaceColor); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Color.Parse("#ad2bee")); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(MutedColor); + + var homeView = new RetroGamingHomeView(); + homeView.GameSelected = PushDetailPage; + + var homeTab = new ContentPage + { + Header = "Home", + Icon = "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + Background = new SolidColorBrush(BgColor), + Content = homeView, + }; + + var gamesView = new RetroGamingGamesView(); + gamesView.GameSelected = PushDetailPage; + + var gamesTab = new ContentPage + { + Header = "Games", + Icon = "M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z", + Background = new SolidColorBrush(BgColor), + Content = gamesView, + }; + + var favTab = new ContentPage + { + Header = "Favorites", + Icon = "M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z", + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingFavoritesView(), + }; + + var profileTab = new ContentPage + { + Header = "Profile", + Icon = "M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingProfileView(), + }; + + tp.Pages = new ObservableCollection { homeTab, gamesTab, favTab, profileTab }; + return tp; + } + + Control BuildSearchFab() + { + var fab = new Button + { + Width = 50, Height = 50, + CornerRadius = new CornerRadius(0), + Background = new SolidColorBrush(YellowColor), + Padding = new Thickness(0), + }; + fab.Classes.Add("retro-fab"); + fab.Content = new PathIcon + { + Width = 22, Height = 22, + Foreground = new SolidColorBrush(BgColor), + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + fab.Click += (_, _) => _ = _nav?.PushModalAsync(BuildSearchModal()); + + return new Border + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 0, 35), + BoxShadow = new BoxShadows(new BoxShadow + { + Blur = 10, Spread = 1, + Color = Color.FromArgb(140, 255, 255, 0), + }), + Child = fab, + }; + } + + ContentPage BuildSearchModal() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + + var searchView = new RetroGamingSearchView(); + searchView.CloseRequested = () => _ = _nav?.PopModalAsync(); + searchView.GameSelected = async title => + { + await (_nav?.PopModalAsync() ?? System.Threading.Tasks.Task.CompletedTask); + PushDetailPage(title); + }; + + page.Content = searchView; + return page; + } + + async void PushDetailPage(string gameTitle) + { + if (_nav == null) return; + + var detailView = new RetroGamingDetailView(gameTitle); + + var page = new ContentPage + { + Background = new SolidColorBrush(BgColor), + Content = detailView, + }; + + NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay); + page.NavigatedTo += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = Brushes.Transparent; }; + page.NavigatedFrom += (_, _) => { if (_nav != null) _nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor); }; + + var cmdBar = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 4, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var heartBtn = new Button(); + heartBtn.Classes.Add("retro-icon-btn"); + heartBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(Color.Parse("#ad2bee")), + Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"), + }; + var shareBtn = new Button(); + shareBtn.Classes.Add("retro-icon-btn"); + shareBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + cmdBar.Children.Add(heartBtn); + cmdBar.Children.Add(shareBtn); + NavigationPage.SetTopCommandBar(page, cmdBar); + + await _nav.PushAsync(page); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml new file mode 100644 index 0000000000..718e8137d2 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs new file mode 100644 index 0000000000..cf49850b84 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingDetailView : UserControl +{ + static readonly Dictionary GameAssets = new() + { + { "Cyber Ninja 2084", "hero.jpg" }, + { "Pixel Quest", "pixel_quest.jpg" }, + { "Neon Racer", "neon_racer.jpg" }, + { "Dungeon Bit", "dungeon_bit.jpg" }, + { "Forest Spirit", "forest_spirit.jpg" }, + { "Cyber City", "cyber_city.jpg" }, + { "Neon Ninja", "neon_ninja.jpg" }, + { "Space Voids", "space_voids.jpg" }, + }; + + public RetroGamingDetailView() => InitializeComponent(); + + public RetroGamingDetailView(string gameTitle) + { + InitializeComponent(); + + DetailTitleText.Text = gameTitle.ToUpperInvariant(); + + var filename = GameAssets.TryGetValue(gameTitle, out var f) ? f + : (GameAssets.TryGetValue("Neon Ninja", out var fb) ? fb : null); + + if (filename != null) + { + try + { + var uri = new Uri($"avares://ControlCatalog/Assets/RetroGaming/{filename}"); + using var stream = AssetLoader.Open(uri); + var bmp = new Bitmap(stream); + DetailHeroImageBorder.Background = new ImageBrush(bmp) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + SetFallbackBackground(); + } + } + else + { + SetFallbackBackground(); + } + } + + void SetFallbackBackground() + { + var grad = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + }; + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#3d2060"), 0)); + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#120a1f"), 1)); + DetailHeroImageBorder.Background = grad; + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml new file mode 100644 index 0000000000..ef11d4e72d --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs new file mode 100644 index 0000000000..fd67902a09 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingFavoritesView : UserControl +{ + public RetroGamingFavoritesView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml new file mode 100644 index 0000000000..a4e9f40387 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs new file mode 100644 index 0000000000..6676b792ab --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingGamesView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingGamesView() + { + InitializeComponent(); + + GameCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + GameNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + GameDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + GameForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + GamePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + GameSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + GameCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + + GamesGrid.SizeChanged += OnGridSizeChanged; + } + + void OnGridSizeChanged(object? sender, SizeChangedEventArgs e) + { + const double defaultWidth = 145; + var available = GamesGrid.Bounds.Width; + if (available <= 0) return; + + bool singleColumn = available < defaultWidth * 2; + foreach (var child in GamesGrid.Children) + { + if (child is Button btn && btn.Content is Border card) + card.Width = singleColumn ? available : defaultWidth; + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml new file mode 100644 index 0000000000..bf5d112531 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs new file mode 100644 index 0000000000..d1693f3c6a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingHomeView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingHomeView() + { + InitializeComponent(); + + HeroPlayBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + ContinuePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + ContinueSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + NewReleaseNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + NewReleaseDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + NewReleaseForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + NewReleaseCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml new file mode 100644 index 0000000000..17d7ff2d5a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs new file mode 100644 index 0000000000..f2284bdc26 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingProfileView : UserControl +{ + public RetroGamingProfileView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml new file mode 100644 index 0000000000..92389efecb --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs new file mode 100644 index 0000000000..c5db19c04d --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? GameSelected { get; set; } + + public RetroGamingSearchView() + { + InitializeComponent(); + + CloseBtn.Click += (_, _) => CloseRequested?.Invoke(); + SearchCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + SearchNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + SearchDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + SearchForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + SearchPixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + SearchSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + SearchCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs new file mode 100644 index 0000000000..0cddd58efd --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: horizontal slide combined with cross-fade. + /// Both pages slide and fade simultaneously for a smooth blended effect. + /// + public class CompositeTransition : IPageTransition + { + public CompositeTransition() { } + + public CompositeTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing TransitionEasing { get; set; } = new LinearEasing(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance : distance), + new Setter(Visual.OpacityProperty, 0d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance), + new Setter(Visual.OpacityProperty, 0d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs new file mode 100644 index 0000000000..ece9b1a1de --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a "fade through" with scale. + /// The outgoing page fades out while scaling down; the incoming page fades in while + /// scaling up, producing a smooth depth-aware transition. + /// + public class FadeThroughTransition : IPageTransition + { + public FadeThroughTransition() { } + + public FadeThroughTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing FadeEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + + if (from != null) + { + from.RenderTransformOrigin = RelativePoint.Center; + from.RenderTransform = new ScaleTransform(1, 1); + } + + if (to != null) + { + to.RenderTransformOrigin = RelativePoint.Center; + to.RenderTransform = new ScaleTransform(1, 1); + to.Opacity = 0; + } + + if (from != null) + { + var outAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 0.92 : 1.08), + new Setter(ScaleTransform.ScaleYProperty, forward ? 0.92 : 1.08) + } + } + } + }; + tasks.Add(outAnim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var inAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 1.08 : 0.92), + new Setter(ScaleTransform.ScaleYProperty, forward ? 1.08 : 0.92) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + } + } + }; + tasks.Add(inAnim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (to != null && !cancellationToken.IsCancellationRequested) + { + to.Opacity = 1; + to.RenderTransform = null; + } + + if (from != null) + { + if (!cancellationToken.IsCancellationRequested) + from.IsVisible = false; + from.Opacity = 1; + from.RenderTransform = null; + } + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs new file mode 100644 index 0000000000..dfbe8478f7 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a directional page slide. + /// Both pages slide together in the specified axis direction. + /// Demonstrates how to implement a custom horizontal or vertical slide from scratch. + /// + public class PageSlideTransition : IPageTransition + { + public enum SlideAxis { Horizontal, Vertical } + + public PageSlideTransition() { } + + public PageSlideTransition(TimeSpan duration, SlideAxis axis = SlideAxis.Horizontal) + { + Duration = duration; + Axis = axis; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public SlideAxis Axis { get; set; } = SlideAxis.Horizontal; + public Easing SlideEasing { get; set; } = new LinearEasing(); + + public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) => + Axis == SlideAxis.Horizontal + ? StartAxis(from, to, forward, cancellationToken, TranslateTransform.XProperty, () => GetVisualParent(from, to).Bounds.Width) + : StartAxis(from, to, forward, cancellationToken, TranslateTransform.YProperty, () => GetVisualParent(from, to).Bounds.Height); + + private async Task StartAxis( + Visual? from, Visual? to, bool forward, CancellationToken cancellationToken, + Avalonia.AvaloniaProperty prop, Func getDistance) + { + if (cancellationToken.IsCancellationRequested) + return; + var tasks = new List(); + var distance = getDistance() is > 0 and var d ? d : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, 0d) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, forward ? -distance : distance) } } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, forward ? distance : -distance) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, 0d) } } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs new file mode 100644 index 0000000000..7326c6e933 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a parallax slide. + /// The incoming page slides full-width from the right while the outgoing page shifts ~30% + /// to the left with a subtle opacity fade, producing a depth-layered push effect. + /// + public class ParallaxSlideTransition : IPageTransition + { + public ParallaxSlideTransition() { } + + public ParallaxSlideTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(350); + public Easing SlideEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance * 0.3 : distance), + new Setter(Visual.OpacityProperty, forward ? 0.7 : 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance * 0.3), + new Setter(Visual.OpacityProperty, forward ? 1d : 0.7) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs b/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs new file mode 100644 index 0000000000..3178c35fae --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for the performance-monitor demo pages + /// (NavigationPage, TabbedPage, DrawerPage, ContentPage). + /// + internal sealed class NavigationPerformanceMonitorHelper + { + internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F")); + internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C")); + internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575")); + internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4")); + internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC")); + + private readonly List> _trackedPages = new(); + private double _previousHeapMB; + private DispatcherTimer? _autoRefreshTimer; + + internal readonly Stopwatch OpStopwatch = new(); + internal int TotalCreated; + + /// + /// Track a newly-created page via WeakReference and increment TotalCreated. + /// + internal void TrackPage(Page page) + { + TotalCreated++; + _trackedPages.Add(new WeakReference(page)); + } + + /// + /// Count live (not yet GC'd) tracked page instances. + /// + internal int CountLiveInstances() + { + int alive = 0; + for (int i = _trackedPages.Count - 1; i >= 0; i--) + { + if (_trackedPages[i].TryGetTarget(out _)) + alive++; + else + _trackedPages.RemoveAt(i); + } + return alive; + } + + /// + /// Update heap and delta text blocks. Call from RefreshAll(). + /// + internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + heapText.Text = $"Managed Heap: {heapMB:##0.0} MB"; + + var delta = heapMB - _previousHeapMB; + if (Math.Abs(delta) < 0.05) + { + deltaText.Text = "(no change)"; + deltaText.Foreground = ZeroDeltaBrush; + } + else + { + var sign = delta > 0 ? "+" : ""; + deltaText.Text = $"({sign}{delta:0.0} MB)"; + deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush; + } + _previousHeapMB = heapMB; + } + + /// + /// Initialize previous heap baseline. + /// + internal void InitHeap() + { + _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + } + + /// + /// Stop the stopwatch and write elapsed ms to the given TextBlock. + /// + internal void StopMetrics(TextBlock lastOpText) + { + if (!OpStopwatch.IsRunning) return; + OpStopwatch.Stop(); + lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms"; + } + + /// + /// Force full GC, then invoke the refresh callback. + /// + internal void ForceGC(Action refresh) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + refresh(); + } + + /// + /// Start a 2-second auto-refresh timer. + /// + internal void StartAutoRefresh(Action refresh) + { + if (_autoRefreshTimer != null) return; + _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + _autoRefreshTimer.Tick += (_, _) => refresh(); + _autoRefreshTimer.Start(); + } + + /// + /// Stop the auto-refresh timer. + /// + internal void StopAutoRefresh() + { + _autoRefreshTimer?.Stop(); + _autoRefreshTimer = null; + } + + /// + /// Toggle auto-refresh based on a CheckBox. + /// + internal void OnAutoRefreshChanged(CheckBox check, Action refresh) + { + if (check.IsChecked == true) + StartAutoRefresh(refresh); + else + StopAutoRefresh(); + } + + /// + /// Append a timestamped log entry to a StackPanel inside a ScrollViewer. + /// + internal void LogOperation(string action, string detail, + StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + var timing = OpStopwatch.ElapsedMilliseconds; + var extra = extraInfo != null ? $" {extraInfo}," : ""; + + logPanel.Children.Add(new TextBlock + { + Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms", + FontSize = 10, + FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), + Padding = new Thickness(6, 2), + TextTrimming = TextTrimming.CharacterEllipsis, + }); + logScroll.ScrollToEnd(); + } + + /// + /// Build a tracked ContentPage with a 50 KB dummy allocation. + /// + internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200) + { + var page = NavigationDemoHelper.MakePage(title, + $"Stack position #{index}\nPush more pages ...", index); + page.Tag = new byte[allocBytes]; + TrackPage(page); + return page; + } + + /// + /// Create a reusable stack/history row (badge + title + label). + /// + internal static (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) CreateStackRow() + { + var indexText = new TextBlock + { + FontSize = 10, FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + Width = 22, Height = 22, + CornerRadius = new CornerRadius(11), + VerticalAlignment = VerticalAlignment.Center, + Child = indexText, + }; + var titleText = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(6, 0, 0, 0), + }; + var badgeText = new TextBlock + { + FontSize = 10, Opacity = 0.5, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4, 0, 0, 0), + IsVisible = false, + }; + + var row = new DockPanel(); + row.Children.Add(badge); + row.Children.Add(titleText); + row.Children.Add(badgeText); + + var container = new Border + { + CornerRadius = new CornerRadius(6), + Padding = new Thickness(8, 6), + Child = row, + }; + + return (container, badge, indexText, titleText, badgeText); + } + + /// + /// Update a stack row with page data. + /// + internal static void UpdateStackRow( + (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) row, + int stackIndex, string title, bool isCurrent, bool isRoot) + { + row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex); + row.IndexText.Text = (stackIndex + 1).ToString(); + row.TitleText.Text = title; + row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal; + + string? label = isCurrent ? "current" : (isRoot ? "root" : null); + row.BadgeText.Text = label ?? ""; + row.BadgeText.IsVisible = label != null; + + row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush; + row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1); + } + + /// + /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed. + /// + internal static void RefreshStackPanel( + StackPanel panel, + List<(Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText)> rowCache, + IReadOnlyList stack, Page? currentPage) + { + int count = stack.Count; + + while (rowCache.Count < count) + rowCache.Add(CreateStackRow()); + + while (panel.Children.Count > count) + panel.Children.RemoveAt(panel.Children.Count - 1); + while (panel.Children.Count < count) + panel.Children.Add(rowCache[panel.Children.Count].Container); + + for (int displayIdx = 0; displayIdx < count; displayIdx++) + { + int stackIdx = count - 1 - displayIdx; + var page = stack[stackIdx]; + bool isCurrent = ReferenceEquals(page, currentPage); + bool isRoot = stackIdx == 0; + + var row = rowCache[displayIdx]; + if (!ReferenceEquals(panel.Children[displayIdx], row.Container)) + panel.Children[displayIdx] = row.Container; + + UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot); + } + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml new file mode 100644 index 0000000000..bb1467060b --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs new file mode 100644 index 0000000000..f4e65249ce --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class TabbedDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic TabbedPage with three tabs, tab placement selector, and selection status.", + () => new TabbedPageFirstLookPage()), + + // Populate + ("Populate", "Page Collection", + "Populate a TabbedPage by adding ContentPage objects directly to the Pages collection.", + () => new TabbedPageCollectionPage()), + ("Populate", "Data Templates", + "Populate a TabbedPage with a data collection and a custom PageTemplate to render each item.", + () => new TabbedPageDataTemplatePage()), + + // Appearance + ("Appearance", "Tab Customization", + "Customize tab placement, bar background, selected and unselected tab colors.", + () => new TabbedPageCustomizationPage()), + ("Appearance", "Custom Tab Bar", + "VYNTRA-style custom tab bar with floating pill, brand colours, and system-adaptive theme using only resource overrides and styles.", + () => new TabbedPageCustomTabBarPage()), + ("Appearance", "FAB Tab Bar", + "Social-media-style bottom nav with a central floating action button that triggers a command, not a tab.", + () => new TabbedPageFabPage()), + ("Appearance", "Fluid Nav Bar", + "Inspired by the Flutter fluid_nav_bar vignette. Color themes with animated indicator and icons.", + () => new TabbedPageFluidNavPage()), + + // Features + ("Features", "Programmatic Selection", + "Preset the initial tab with SelectedIndex, jump to any tab programmatically, and respond to SelectionChanged events.", + () => new TabbedPageProgrammaticPage()), + ("Features", "Placement", "Switch the tab bar between Top, Bottom, Left, and Right placements.", + () => new TabbedPagePlacementPage()), + ("Features", "Page Transitions", + "Animate tab switches with CrossFade, PageSlide, or composite transitions.", + () => new TabbedPageTransitionsPage()), + ("Features", "Keyboard Navigation", + "Keyboard shortcuts to navigate between tabs, with a toggle to enable or disable.", + () => new TabbedPageKeyboardPage()), + ("Features", "Swipe Gestures", + "Swipe left/right (Top/Bottom) or up/down (Left/Right) to navigate. Toggle IsGestureEnabled.", + () => new TabbedPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Switch tabs to see the live event log.", + () => new TabbedPageEventsPage()), + ("Features", "Disabled Tabs", + "IsTabEnabled attached property: disable individual tabs so they cannot be selected.", + () => new TabbedPageDisabledTabsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track tab count, live page instances, and managed heap size. Observe how GC reclaims memory after removing tabs.", + () => new TabbedPagePerformancePage()), + + // Composition + ("Composition", "With NavigationPage", + "Embed a NavigationPage inside each TabbedPage tab for drill-down navigation.", + () => new TabbedPageWithNavigationPage()), + ("Composition", "With DrawerPage", + "Combine TabbedPage with DrawerPage: a global navigation drawer sits over tabbed content.", + () => new TabbedPageWithDrawerPage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Fitness app with bottom TabbedPage navigation, NavigationPage drill-down inside tabs, and workout detail screens.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir Restaurant", + "Restaurant app with DrawerPage root, NavigationPage detail, and TabbedPage bottom tabs for Menu, Reservations, and Profile.", + () => new LAvenirAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + }; + + public TabbedDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs b/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs new file mode 100644 index 0000000000..6ca5e0ec3e --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + /// + /// A custom panel that arranges N children in N+1 equally-sized slots, leaving the + /// middle slot empty. Intended for tab bars that need a central action button + /// overlaid on the gap. + /// + /// For N children the split is ⌊N/2⌋ items on the left and ⌈N/2⌉ on the right. + /// Example – 4 tabs: [0][1][ gap ][2][3] (5 equal columns, center is free). + /// + public class CenteredTabPanel : Panel + { + protected override Size MeasureOverride(Size availableSize) + { + int count = Children.Count; + if (count == 0) + return default; + + int slots = count + 1; + bool infiniteWidth = double.IsInfinity(availableSize.Width); + double slotWidth = infiniteWidth ? 60.0 : availableSize.Width / slots; + + double maxHeight = 0; + foreach (var child in Children) + { + child.Measure(new Size(slotWidth, availableSize.Height)); + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + } + + // When given finite width, fill it. When infinite (inside a ScrollViewer), + // return a small positive width so the parent allocates real space. + double desiredWidth = infiniteWidth ? slotWidth * slots : availableSize.Width; + if (double.IsNaN(maxHeight) || double.IsInfinity(maxHeight)) + maxHeight = 0; + + return new Size(desiredWidth, maxHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + int count = Children.Count; + if (count == 0) + return finalSize; + + int slots = count + 1; + double slotW = finalSize.Width / slots; + int leftCount = count / 2; // items placed to the left of the gap + + for (int i = 0; i < count; i++) + { + // Skip the center slot (leftCount), reserved for the FAB. + int slot = i < leftCount ? i : i + 1; + Children[i].Arrange(new Rect(slot * slotW, 0, slotW, finalSize.Height)); + } + + return finalSize; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs new file mode 100644 index 0000000000..a68e57c9a4 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SkiaSharp; + +namespace ControlCatalog.Pages +{ + /// + /// A fluid navigation bar that replicates the Flutter fluid_nav_bar vignette. + /// The bar background has a bezier "dip" that travels to the selected tab. + /// Each icon is drawn progressively using SKPathMeasure for the fill animation. + /// + public class FluidNavBar : Control, Avalonia.Rendering.ICustomHitTest + { + internal const double NominalHeight = 56.0; + internal const double CircleRadius = 25.0; + internal const double ActiveFloat = 16.0; // px the circle rises + internal const double IconDrawScale = 0.9; // icon scale within circle + internal const double ScaleCurveScale = 0.50; + internal const double FloatLinearPIn = 0.28; + internal const double FillLinearPIn = 0.25; + internal const double XAnimDuration = 0.620; // s — X bump travel + internal const double YDipDuration = 0.300; // s — dip down + internal const double YBounceDelay = 0.500; // s — wait before bounce + internal const double YBounceDuration = 1.200; // s — elastic bounce up + internal const double FloatUpDuration = 1.666; // s — circle rising + internal const double FloatDownDuration = 0.833; // s — circle falling + + public static readonly StyledProperty> ItemsProperty = + AvaloniaProperty.Register>( + nameof(Items), new List()); + + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register(nameof(SelectedIndex), 0); + + public static readonly StyledProperty BarColorProperty = + AvaloniaProperty.Register(nameof(BarColor), Colors.White); + + public static readonly StyledProperty ButtonColorProperty = + AvaloniaProperty.Register(nameof(ButtonColor), Colors.White); + + public static readonly StyledProperty ActiveIconColorProperty = + AvaloniaProperty.Register(nameof(ActiveIconColor), Colors.Black); + + public static readonly StyledProperty InactiveIconColorProperty = + AvaloniaProperty.Register( + nameof(InactiveIconColor), Color.FromArgb(140, 120, 120, 120)); + + private double _xCurrent = -1; // -1 = not yet initialised + private double _lastWidth = -1; // tracks width changes for resize correction + private double _xStart, _xTarget, _xAnimStartSec; + private double _yValue = 1.0; // 0 = deepest dip, 1 = flat + private double _yDipStartSec; + private bool _yBounceStarted; + private double _yBounceStartSec; + + // per-item (length = Items.Count after OnItemsChanged) + private double[] _floatProgress = Array.Empty(); + private double[] _floatStartSec = Array.Empty(); + private bool[] _floatGoingUp = Array.Empty(); + + // Parsed Skia paths — owned here, disposed on detach / items change + private SKPath?[] _parsedPaths = Array.Empty(); + + private DispatcherTimer? _animTimer; + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private bool _animating; + + public IList Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + + public Color BarColor + { + get => GetValue(BarColorProperty); + set => SetValue(BarColorProperty, value); + } + + public Color ButtonColor + { + get => GetValue(ButtonColorProperty); + set => SetValue(ButtonColorProperty, value); + } + + public Color ActiveIconColor + { + get => GetValue(ActiveIconColorProperty); + set => SetValue(ActiveIconColorProperty, value); + } + + public Color InactiveIconColor + { + get => GetValue(InactiveIconColorProperty); + set => SetValue(InactiveIconColorProperty, value); + } + + public event EventHandler? SelectionChanged; + + public FluidNavBar() + { + ClipToBounds = false; + Height = NominalHeight; + Cursor = new Cursor(StandardCursorType.Hand); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemsProperty) + OnItemsChanged(); + else if (change.Property == SelectedIndexProperty) + OnSelectedIndexChanged(change.GetOldValue(), change.GetNewValue()); + else if (change.Property == BarColorProperty + || change.Property == ButtonColorProperty + || change.Property == ActiveIconColorProperty + || change.Property == InactiveIconColorProperty) + InvalidateVisual(); + } + + public bool HitTest(Point point) + { + return point.X >= 0 && point.X <= Bounds.Width + && point.Y >= -(ActiveFloat + CircleRadius) + && point.Y <= Bounds.Height; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var n = Items?.Count ?? 0; + if (n == 0 || Bounds.Width <= 0) return; + + var pos = e.GetPosition(this); + var index = (int)(pos.X / (Bounds.Width / n)); + index = Math.Clamp(index, 0, n - 1); + + if (index != SelectedIndex) + { + SetCurrentValue(SelectedIndexProperty, index); + SelectionChanged?.Invoke(this, index); + } + + e.Handled = true; + } + + protected override Size MeasureOverride(Size availableSize) + { + var w = double.IsPositiveInfinity(availableSize.Width) ? 300 : availableSize.Width; + return new Size(w, NominalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var w = finalSize.Width; + if (w > 0) + { + if (_xCurrent < 0 || _lastWidth < 0) + { + // First layout — snap everything to the current selection + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + _xStart = _xCurrent; + } + else if (Math.Abs(w - _lastWidth) > 0.5) + { + // Width changed (resize) — scale pixel positions proportionally + // so the bump stays over the correct slot + var ratio = w / _lastWidth; + _xCurrent = _xCurrent * ratio; + _xStart = _xStart * ratio; + _xTarget = IndexToX(SelectedIndex, w); + InvalidateVisual(); + } + _lastWidth = w; + } + return new Size(w > 0 ? w : 300, NominalHeight); + } + + public override void Render(DrawingContext context) + { + var w = Bounds.Width; + var h = Bounds.Height; + + if (w <= 0 || h <= 0 || Items == null || Items.Count == 0) return; + + var n = Items.Count; + + // Initialise _xCurrent if layout didn't run yet + if (_xCurrent < 0) + { + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + } + + // Snapshot per-item animation state for this frame + var slotCenters = new double[n]; + var floatOffsets = new double[n]; + var scaleYValues = new double[n]; + var fillAmounts = new double[n]; + + for (int i = 0; i < n; i++) + slotCenters[i] = IndexToX(i, w); + + for (int i = 0; i < n; i++) + { + var p = i < _floatProgress.Length ? _floatProgress[i] : (i == SelectedIndex ? 1.0 : 0.0); + var goUp = i < _floatGoingUp.Length ? _floatGoingUp[i] : (i == SelectedIndex); + + // Float offset — uses LinearPoint(0.28, 0) to delay start, then elastic/quintic easing + var linearP = LinearPoint(p, FloatLinearPIn, 0.0); + var floatEased = goUp ? ElasticOut(linearP, 0.38) : EaseInQuint(linearP); + floatOffsets[i] = ActiveFloat * floatEased; + + // Scale Y squish via CenteredElastic curves + var centered = goUp ? CenteredElasticOut(p, 0.6) : CenteredElasticIn(p, 0.6); + scaleYValues[i] = 0.75 + centered * ScaleCurveScale; + + // Icon fill — LinearPoint(0.25, 1.0) adds a slight draw delay vs float + fillAmounts[i] = LinearPoint(p, FillLinearPIn, 1.0); + } + + // Clamp scaleY to sane range to avoid SVG-transform oddities + for (int i = 0; i < n; i++) + scaleYValues[i] = Math.Max(0.1, Math.Min(1.5, scaleYValues[i])); + + var op = new FluidNavBarRenderOp( + new Rect(0, -(ActiveFloat + CircleRadius), w, h + ActiveFloat + CircleRadius), + (float)w, (float)h, + (float)_xCurrent, (float)_yValue, + slotCenters, floatOffsets, scaleYValues, fillAmounts, + _parsedPaths, + BarColor, ButtonColor, ActiveIconColor, InactiveIconColor); + + context.Custom(op); + } + + private void OnItemsChanged() + { + foreach (var p in _parsedPaths) p?.Dispose(); + + var n = Items?.Count ?? 0; + _parsedPaths = new SKPath?[n]; + _floatProgress = new double[n]; + _floatStartSec = new double[n]; + _floatGoingUp = new bool[n]; + + for (int i = 0; i < n; i++) + { + var svg = Items![i].SvgPath; + if (!string.IsNullOrEmpty(svg)) + _parsedPaths[i] = SKPath.ParseSvgPathData(svg); + } + + var sel = Math.Clamp(SelectedIndex, 0, Math.Max(0, n - 1)); + for (int i = 0; i < n; i++) + { + _floatProgress[i] = i == sel ? 1.0 : 0.0; + _floatGoingUp[i] = i == sel; + } + + _xCurrent = -1; // force re-init on next arrange/render + InvalidateVisual(); + } + + private void OnSelectedIndexChanged(int oldIndex, int newIndex) + { + var n = _floatProgress.Length; + if (n == 0) return; + + newIndex = Math.Clamp(newIndex, 0, n - 1); + oldIndex = Math.Clamp(oldIndex, 0, n - 1); + if (oldIndex == newIndex) return; + + var now = _clock.Elapsed.TotalSeconds; + + // X: slide bump from old to new position + if (_xCurrent < 0 && Bounds.Width > 0) + _xCurrent = IndexToX(oldIndex, Bounds.Width); + + _xStart = _xCurrent; + _xTarget = Bounds.Width > 0 ? IndexToX(newIndex, Bounds.Width) : _xStart; + _xAnimStartSec = now; + + // Y: dip then elastic bounce + _yValue = 1.0; + _yDipStartSec = now; + _yBounceStarted = false; + + // Per-button float + _floatGoingUp[oldIndex] = false; + _floatStartSec[oldIndex] = now; + _floatGoingUp[newIndex] = true; + _floatStartSec[newIndex] = now; + + StartAnimation(); + } + + private void StartAnimation() + { + if (_animating) return; + _animating = true; + _animTimer = new DispatcherTimer( + TimeSpan.FromSeconds(1.0 / 60.0), + DispatcherPriority.Render, + OnAnimTick); + _animTimer.Start(); + } + + private void StopAnimation() + { + _animTimer?.Stop(); + _animTimer = null; + _animating = false; + } + + private void OnAnimTick(object? sender, EventArgs e) + { + var now = _clock.Elapsed.TotalSeconds; + var anyActive = false; + + var xElapsed = now - _xAnimStartSec; + if (xElapsed < XAnimDuration) + { + _xCurrent = _xStart + (_xTarget - _xStart) * (xElapsed / XAnimDuration); + anyActive = true; + } + else + { + _xCurrent = _xTarget; + } + + var yDipElapsed = now - _yDipStartSec; + if (yDipElapsed < YDipDuration) + { + _yValue = 1.0 - yDipElapsed / YDipDuration; + anyActive = true; + } + else + { + _yValue = 0.0; + + if (!_yBounceStarted && yDipElapsed >= YBounceDelay) + { + _yBounceStarted = true; + _yBounceStartSec = now; + } + + if (_yBounceStarted) + { + var bt = now - _yBounceStartSec; + if (bt < YBounceDuration) + { + _yValue = ElasticOut(bt / YBounceDuration, 0.38); + anyActive = true; + } + else + { + _yValue = 1.0; + } + } + } + + for (int i = 0; i < _floatProgress.Length; i++) + { + var elapsed = now - _floatStartSec[i]; + var duration = _floatGoingUp[i] ? FloatUpDuration : FloatDownDuration; + if (elapsed < duration) + { + var t = elapsed / duration; + _floatProgress[i] = _floatGoingUp[i] ? t : 1.0 - t; + anyActive = true; + } + else + { + _floatProgress[i] = _floatGoingUp[i] ? 1.0 : 0.0; + } + } + + InvalidateVisual(); + + if (!anyActive) + StopAnimation(); + } + + private double IndexToX(int index, double width) + { + var n = Items?.Count ?? 1; + if (n <= 0) n = 1; + return (index + 0.5) * (width / n); + } + + + internal static double ElasticOut(double t, double period = 0.4) + { + if (t <= 0) return 0; + if (t >= 1) return 1; + var s = period / 4.0; + return Math.Pow(2.0, -10.0 * t) * Math.Sin((t - s) * 2.0 * Math.PI / period) + 1.0; + } + + private static double CenteredElasticOut(double t, double period = 0.4) + { + return Math.Pow(2.0, -10.0 * t) * Math.Sin(t * 2.0 * Math.PI / period) + 0.5; + } + + private static double CenteredElasticIn(double t, double period = 0.4) + { + return -Math.Pow(2.0, 10.0 * (t - 1.0)) * Math.Sin((t - 1.0) * 2.0 * Math.PI / period) + 0.5; + } + + internal static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lowerScale = pOut / pIn; + var upperScale = (1.0 - pOut) / (1.0 - pIn); + var upperOff = 1.0 - upperScale; + return x < pIn ? x * lowerScale : x * upperScale + upperOff; + } + + private static double EaseInQuint(double t) => t * t * t * t * t; + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + StopAnimation(); + } + + private sealed class FluidNavBarRenderOp : ICustomDrawOperation + { + private readonly float _w, _h, _xCenter, _normY; + private readonly double[] _slots, _floatOff, _scaleY, _fill; + private readonly SKPath?[] _paths; + private readonly Color _bar, _btn, _active, _inactive; + + public Rect Bounds { get; } + + public FluidNavBarRenderOp( + Rect bounds, + float w, float h, + float xCenter, float normY, + double[] slots, double[] floatOff, double[] scaleY, double[] fill, + SKPath?[] paths, + Color bar, Color btn, Color active, Color inactive) + { + Bounds = bounds; + _w = w; _h = h; + _xCenter = xCenter; _normY = normY; + _slots = slots; _floatOff = floatOff; + _scaleY = scaleY; _fill = fill; + _paths = paths; + _bar = bar; _btn = btn; + _active = active; _inactive = inactive; + } + + public bool HitTest(Point p) => false; + public bool Equals(ICustomDrawOperation? other) => false; + public void Dispose() { } + + public void Render(ImmediateDrawingContext context) + { + var lease = context.TryGetFeature(); + if (lease == null) return; + + using var l = lease.Lease(); + var canvas = l.SkCanvas; + + int save = canvas.Save(); + try + { + DrawBackground(canvas); + for (int i = 0; i < _slots.Length; i++) + DrawButton(canvas, i); + } + finally + { + canvas.RestoreToCount(save); + } + } + + private void DrawBackground(SKCanvas canvas) + { + const float rTop = 54f, rBot = 44f; + const float hcTop = 0.6f, hcBot = 0.5f; + const float pcTop = 0.35f, pcBot = 0.85f; + const float tY = -10f, bY = 54f; + const float tD = 0f, bD = 6f; + + float norm = (float)(LinearPoint(_normY, 0.5, 2.0) / 2.0); + + float r = Lerp(rTop, rBot, norm); + float anchr = Lerp(r * hcTop, r * hcBot, (float)LinearPoint(norm, 0.5, 0.75)); + float dipc = Lerp(r * pcTop, r * pcBot, (float)LinearPoint(norm, 0.5, 0.80)); + float y = Lerp(tY, bY, (float)LinearPoint(norm, 0.2, 0.70)); + float dist = Lerp(tD, bD, (float)LinearPoint(norm, 0.5, 0.00)); + float x0 = _xCenter - dist / 2f; + float x1 = _xCenter + dist / 2f; + + using var path = new SKPath(); + path.MoveTo(0, 0); + path.LineTo(x0 - r, 0); + path.CubicTo(x0 - r + anchr, 0, x0 - dipc, y, x0, y); + path.LineTo(x1, y); + path.CubicTo(x1 + dipc, y, x1 + r - anchr, 0, x1 + r, 0); + path.LineTo(_w, 0); + path.LineTo(_w, _h); + path.LineTo(0, _h); + path.Close(); + + using var paint = new SKPaint { Color = ToSK(_bar), IsAntialias = true }; + canvas.DrawPath(path, paint); + } + + private void DrawButton(SKCanvas canvas, int i) + { + var cx = (float)_slots[i]; + var cy = _h / 2f; + var fo = (float)_floatOff[i]; + var sy = (float)_scaleY[i]; + var fa = (float)_fill[i]; + + const float r = (float)CircleRadius; + + // Circle — just translated up, not scaled + using var cp = new SKPaint { Color = ToSK(_btn), IsAntialias = true }; + canvas.DrawCircle(cx, cy - fo, r, cp); + + // Icon + if (i < _paths.Length && _paths[i] != null) + DrawIcon(canvas, _paths[i]!, cx, cy - fo, sy, fa); + } + + private void DrawIcon(SKCanvas canvas, SKPath path, float cx, float cy, + float scaleY, float fillAmount) + { + const float s = (float)IconDrawScale; + + int save = canvas.Save(); + canvas.Translate(cx, cy); + canvas.Scale(s, s * scaleY); + + // Grey background stroke (full path, unselected look) + using var bg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_inactive), + IsAntialias = true + }; + canvas.DrawPath(path, bg); + + // Foreground stroke, trimmed progressively with SKPathMeasure + if (fillAmount > 0f) + { + using var fg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_active), + IsAntialias = true + }; + DrawTrimmedPath(canvas, path, fillAmount, fg); + } + + canvas.RestoreToCount(save); + } + + // Iterates all contours and draws each trimmed to fillAmount of its length. + // Direct port of Flutter's extractPartialPath behavior. + private static void DrawTrimmedPath(SKCanvas canvas, SKPath path, + float fillAmount, SKPaint paint) + { + using var measure = new SKPathMeasure(path, false); + do + { + var len = measure.Length; + if (len <= 0f) continue; + + using var seg = new SKPath(); + if (measure.GetSegment(0f, len * fillAmount, seg, true)) + canvas.DrawPath(seg, paint); + } + while (measure.NextContour()); + } + + private static float Lerp(float a, float b, float t) => a + (b - a) * t; + + private static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lo = pOut / pIn; + var hi = (1.0 - pOut) / (1.0 - pIn); + return x < pIn ? x * lo : x * hi + (1.0 - hi); + } + + private static SKColor ToSK(Color c) => new SKColor(c.R, c.G, c.B, c.A); + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs new file mode 100644 index 0000000000..e2c980050e --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs @@ -0,0 +1,14 @@ +namespace ControlCatalog.Pages +{ + public class FluidNavItem + { + public string SvgPath { get; } + public string Label { get; } + + public FluidNavItem(string svgPath, string label) + { + SvgPath = svgPath; + Label = label; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml new file mode 100644 index 0000000000..80e63a08a1 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs new file mode 100644 index 0000000000..5c10a50df7 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageFabPage : UserControl + { + private static readonly StreamGeometry FeedGeometry = + StreamGeometry.Parse("M12.9942 2.79444C12.4118 2.30208 11.5882 2.30208 11.0058 2.79444L3.50582 9.39444C3.18607 9.66478 3 10.0634 3 10.4828V20.25C3 20.9404 3.55964 21.5 4.25 21.5H8.25C8.94036 21.5 9.5 20.9404 9.5 20.25V14.75C9.5 14.6119 9.61193 14.5 9.75 14.5H14.25C14.3881 14.5 14.5 14.6119 14.5 14.75V20.25C14.5 20.9404 15.0596 21.5 15.75 21.5H19.75C20.4404 21.5 21 20.9404 21 20.25V10.4828C21 10.0634 20.8139 9.66478 20.4942 9.39444L12.9942 2.79444Z"); + private static readonly StreamGeometry DiscoverGeometry = + StreamGeometry.Parse("M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm4.24 5.76-3.03 6.55-6.55 3.03L9.69 10.8l6.55-3.04zM12 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"); + private static readonly StreamGeometry AlertsGeometry = + StreamGeometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"); + private static readonly StreamGeometry ProfileGeometry = + StreamGeometry.Parse("M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z"); + + private int _postCount; + + public TabbedPageFabPage() + { + InitializeComponent(); + SetupIcons(); + + FabButton.Click += OnFabClicked; + TriggerFabButton.Click += OnFabClicked; + } + + private void SetupIcons() + { + FeedPage.Icon = FeedGeometry; + DiscoverPage.Icon = DiscoverGeometry; + AlertsPage.Icon = AlertsGeometry; + ProfilePage.Icon = ProfileGeometry; + } + + private void OnFabClicked(object? sender, RoutedEventArgs e) + { + _postCount++; + StatusText.Text = _postCount == 1 + ? "Post created! Check your feed." + : $"{_postCount} posts created!"; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml new file mode 100644 index 0000000000..1dabb6ba0a --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs new file mode 100644 index 0000000000..88f688d746 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs @@ -0,0 +1,114 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithDrawerPage : UserControl + { + public TabbedPageWithDrawerPage() + { + InitializeComponent(); + Loaded += (_, _) => ShowSection("Home"); + } + + private void OnSectionSelected(object? sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is string section) + { + ShowSection(section); + DemoDrawer.IsOpen = false; + } + } + + private void ShowSection(string section) + { + SectionHost.Content = section switch + { + "Home" => CreateHomeTabbed(), + _ => CreatePlainPage(section) + }; + } + + private static Control CreatePlainPage(string section) + { + var (subtitle, icon) = section switch + { + "Explore" => ("Discover new content.", "📍"), + "Favorites" => ("Items you've saved.", "❤"), + _ => (string.Empty, string.Empty) + }; + + return new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = section, + FontSize = 22, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = subtitle, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 300 + } + } + }; + } + + private static TabbedPage CreateHomeTabbed() => new() + { + TabPlacement = TabPlacement.Bottom, + Pages = new[] + { + new ContentPage + { + Header = "Featured", + Content = new TextBlock + { + Text = "Featured content", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Recent", + Content = new TextBlock + { + Text = "Recent activity", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Popular", + Content = new TextBlock + { + Text = "Popular right now", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + } + } + }; + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml new file mode 100644 index 0000000000..1c2c51ae2a --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs new file mode 100644 index 0000000000..e4efec576a --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs @@ -0,0 +1,94 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithNavigationPage : UserControl + { + public TabbedPageWithNavigationPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await BrowseNav.PushAsync(CreateListPage("Browse", "Items", BrowseNav), null); + await SearchNav.PushAsync(CreateListPage("Search", "Results", SearchNav), null); + await AccountNav.PushAsync(CreateListPage("Account", "Options", AccountNav), null); + } + + private static ContentPage CreateListPage(string tabName, string listTitle, NavigationPage nav) + { + var list = new ListBox + { + Margin = new Avalonia.Thickness(8), + Items = + { + $"{listTitle} item 1", + $"{listTitle} item 2", + $"{listTitle} item 3", + $"{listTitle} item 4", + $"{listTitle} item 5" + } + }; + + list.SelectionChanged += async (_, args) => + { + if (args.AddedItems.Count == 0) return; + var item = args.AddedItems[0]?.ToString() ?? string.Empty; + list.SelectedItem = null; + + var detail = new ContentPage + { + Header = item, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = item, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = $"Detail view for \"{item}\" in the {tabName} tab.", + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = Avalonia.Media.TextAlignment.Center, + MaxWidth = 280 + } + } + } + }; + + await nav.PushAsync(detail, nav.PageTransition); + }; + + var page = new ContentPage + { + Header = tabName, + Content = list, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoTabs == null) return; + DemoTabs.TabPlacement = PlacementCombo.SelectedIndex == 0 ? TabPlacement.Top : TabPlacement.Bottom; + } + } +} diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index a647d34356..df84c0bd6b 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -23,22 +23,13 @@ - - - - diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 716675f2c6..ce9209f61a 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Dialogs; -using Avalonia.Platform; using System; using System.ComponentModel.DataAnnotations; using Avalonia; @@ -13,10 +12,7 @@ namespace ControlCatalog.ViewModels { private WindowState _windowState; private WindowState[] _windowStates = Array.Empty(); - private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private bool _extendClientAreaEnabled; - private bool _systemTitleBarEnabled; - private bool _preferSystemChromeEnabled; private double _titleBarHeight; private bool _isSystemBarVisible; private bool _displayEdgeToEdge; @@ -51,65 +47,16 @@ namespace ControlCatalog.ViewModels WindowState.FullScreen, }; - PropertyChanged += (s, e) => - { - if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled)) - { - var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; - - if (SystemTitleBarEnabled) - { - hints |= ExtendClientAreaChromeHints.SystemChrome; - } - if (PreferSystemChromeEnabled) - { - hints |= ExtendClientAreaChromeHints.PreferSystemChrome; - } - ChromeHints = hints; - } - }; - - SystemTitleBarEnabled = true; TitleBarHeight = -1; CanResize = true; CanMinimize = true; CanMaximize = true; } - public ExtendClientAreaChromeHints ChromeHints - { - get { return _chromeHints; } - set { RaiseAndSetIfChanged(ref _chromeHints, value); } - } - public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value) - { - SystemTitleBarEnabled = true; - } - } - } - - public bool SystemTitleBarEnabled - { - get { return _systemTitleBarEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value) - { - TitleBarHeight = -1; - } - } - } - - public bool PreferSystemChromeEnabled - { - get { return _preferSystemChromeEnabled; } - set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } } public double TitleBarHeight diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 894c052d6c..7d6a2c2c3e 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using IntegrationTestApp.Models; using IntegrationTestApp.Pages; using IntegrationTestApp.ViewModels; +using Page = IntegrationTestApp.Models.Page; namespace IntegrationTestApp { diff --git a/samples/IntegrationTestApp/Pages/GesturesPage.axaml b/samples/IntegrationTestApp/Pages/GesturesPage.axaml index eb028e226f..e171cc62f9 100644 --- a/samples/IntegrationTestApp/Pages/GesturesPage.axaml +++ b/samples/IntegrationTestApp/Pages/GesturesPage.axaml @@ -19,7 +19,7 @@ AutomationProperties.ControlTypeOverride="Image" Tapped="GestureBorder_Tapped" DoubleTapped="GestureBorder_DoubleTapped" - Gestures.RightTapped="GestureBorder_RightTapped"/> + RightTapped="GestureBorder_RightTapped"/> - - - - + diff --git a/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs index baae2b8766..cc94c4c250 100644 --- a/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs +++ b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs @@ -51,7 +51,7 @@ public partial class WindowPage : UserControl ShowWindowSize.Text = string.Empty; window.ExtendClientAreaToDecorationsHint = ShowWindowExtendClientAreaToDecorationsHint.IsChecked ?? false; - window.SystemDecorations = (SystemDecorations)ShowWindowSystemDecorations.SelectedIndex; + window.WindowDecorations = (WindowDecorations)ShowWindowSystemDecorations.SelectedIndex; window.WindowState = (WindowState)ShowWindowState.SelectedIndex; switch (ShowWindowMode.SelectedIndex) @@ -87,7 +87,7 @@ public partial class WindowPage : UserControl { Title = "Transparent Window", Name = "TransparentWindow", - SystemDecorations = SystemDecorations.None, + WindowDecorations = WindowDecorations.None, Background = Brushes.Transparent, TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent }, WindowStartupLocation = WindowStartupLocation.CenterOwner, @@ -136,7 +136,7 @@ public partial class WindowPage : UserControl Width = 200, Height = 200, Background = Brushes.Green, - SystemDecorations = SystemDecorations.None, + WindowDecorations = WindowDecorations.None, WindowStartupLocation = WindowStartupLocation.CenterOwner, Content = new Border { diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 272c61ed0c..38f096e478 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -45,7 +45,7 @@ - + None BorderOnly Full diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml deleted file mode 100644 index 817142fefd..0000000000 --- a/samples/Previewer/App.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/Previewer/App.xaml.cs b/samples/Previewer/App.xaml.cs deleted file mode 100644 index ab83d45cd3..0000000000 --- a/samples/Previewer/App.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace Previewer -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } - -} diff --git a/samples/Previewer/Center.cs b/samples/Previewer/Center.cs deleted file mode 100644 index 7a28827d61..0000000000 --- a/samples/Previewer/Center.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia; -using Avalonia.Controls; - -namespace Previewer -{ - public class Center : Decorator - { - protected override Size ArrangeOverride(Size finalSize) - { - if (Child != null) - { - var desired = Child.DesiredSize; - Child.Arrange(new Rect((finalSize.Width - desired.Width) / 2, (finalSize.Height - desired.Height) / 2, - desired.Width, desired.Height)); - } - return finalSize; - } - } -} \ No newline at end of file diff --git a/samples/Previewer/MainWindow.xaml b/samples/Previewer/MainWindow.xaml deleted file mode 100644 index eb612303f2..0000000000 --- a/samples/Previewer/MainWindow.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/samples/Previewer/MainWindow.xaml.cs b/samples/Previewer/MainWindow.xaml.cs deleted file mode 100644 index dabf90f5d0..0000000000 --- a/samples/Previewer/MainWindow.xaml.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Net; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Remote; -using Avalonia.Markup.Xaml; -using Avalonia.Remote.Protocol; -using Avalonia.Remote.Protocol.Designer; -using Avalonia.Remote.Protocol.Viewport; -using Avalonia.Threading; - -namespace Previewer -{ - public class MainWindow : Window - { - private const string InitialXaml = @" - Hello world! - - "; - private IAvaloniaRemoteTransportConnection? _connection; - private Control _errorsContainer; - private TextBlock _errors; - private RemoteWidget? _remote; - - - public MainWindow() - { - this.InitializeComponent(); - var tb = this.GetControl("Xaml"); - tb.Text = InitialXaml; - var scroll = this.GetControl("Remote"); - var rem = new Center(); - scroll.Content = rem; - _errorsContainer = this.GetControl("ErrorsContainer"); - _errors = this.GetControl("Errors"); - tb.GetObservable(TextBox.TextProperty).Subscribe(text => _connection?.Send(new UpdateXamlMessage - { - Xaml = text - })); - new BsonTcpTransport().Listen(IPAddress.Loopback, 25000, t => - { - Dispatcher.UIThread.Post(() => - { - if (_connection != null) - { - _connection.Dispose(); - _connection.OnMessage -= OnMessage; - } - _connection = t; - rem.Child = _remote = new RemoteWidget(t); - t.Send(new UpdateXamlMessage - { - Xaml = tb.Text - }); - - t.OnMessage += OnMessage; - }); - }); - Title = "Listening on 127.0.0.1:25000"; - } - - private void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) - { - Dispatcher.UIThread.Post(() => - { - if (transport != _connection) - return; - if (obj is UpdateXamlResultMessage result) - { - _errorsContainer.IsVisible = result.Error != null; - _errors.Text = result.Error ?? ""; - } - if (obj is RequestViewportResizeMessage resize && _remote is not null) - { - _remote.Width = Math.Min(4096, Math.Max(resize.Width, 1)); - _remote.Height = Math.Min(4096, Math.Max(resize.Height, 1)); - } - }); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj deleted file mode 100644 index e894108c0f..0000000000 --- a/samples/Previewer/Previewer.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - Exe - $(AvsCurrentTargetFramework) - - - - %(Filename) - - - - - - - - - - - - - - diff --git a/samples/Previewer/Program.cs b/samples/Previewer/Program.cs deleted file mode 100644 index b12b93974a..0000000000 --- a/samples/Previewer/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia; - -namespace Previewer -{ - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect(); - - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } -} diff --git a/samples/RemoteDemo/Program.cs b/samples/RemoteDemo/Program.cs deleted file mode 100644 index 0565b676fb..0000000000 --- a/samples/RemoteDemo/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Remote; -using Avalonia.Remote.Protocol; -using Avalonia.Threading; -using ControlCatalog; - -namespace RemoteDemo -{ - class Program - { - static void Main(string[] args) - { - AppBuilder.Configure().UsePlatformDetect().SetupWithoutStarting(); - - var l = new TcpListener(IPAddress.Loopback, 0); - l.Start(); - var port = ((IPEndPoint) l.LocalEndpoint).Port; - l.Stop(); - - var transport = new BsonTcpTransport(); - transport.Listen(IPAddress.Loopback, port, sc => - { - Dispatcher.UIThread.Post(() => - { - new RemoteServer(sc).Content = new MainView(); - }); - }); - - var cts = new CancellationTokenSource(); - transport.Connect(IPAddress.Loopback, port).ContinueWith(t => - { - Dispatcher.UIThread.Post(() => - { - var window = new Window() - { - Content = new RemoteWidget(t.Result) - }; - window.Closed += delegate { cts.Cancel(); }; - window.Show(); - }); - }); - Dispatcher.UIThread.MainLoop(cts.Token); - - - - } - } -} diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj deleted file mode 100644 index 8c479b1093..0000000000 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - Exe - $(AvsCurrentTargetFramework) - - - - - - - - diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 4ab09e9584..cffecb5b7d 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -216,7 +216,8 @@ - + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 5316a84570..7a3059cb65 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -81,12 +81,12 @@ namespace Avalonia.Android { Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); + Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl()); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(new AndroidDispatcherImpl()) .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToSingleton() diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs index 6293637d4e..e60524c2b7 100644 --- a/src/Android/Avalonia.Android/CursorFactory.cs +++ b/src/Android/Avalonia.Android/CursorFactory.cs @@ -5,7 +5,7 @@ namespace Avalonia.Android { internal class CursorFactory : ICursorFactory { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; + public ICursorImpl CreateCursor(Avalonia.Media.Imaging.Bitmap cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs index e096d32f48..596f09fda3 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs @@ -1,5 +1,5 @@ using System; -using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform.Surfaces; using Avalonia.Platform; namespace Avalonia.Android.Platform.SkiaPlatform diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b11d35d1ef..20284906be 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -18,6 +18,7 @@ using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.OpenGL.Egl; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Java.Lang; @@ -96,7 +97,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public double DesktopScaling => RenderScaling; public IPlatformHandle Handle { get; } - public IEnumerable Surfaces { get; } + public IPlatformRenderSurface[] Surfaces { get; } public Compositor Compositor => AndroidPlatform.Compositor ?? throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed."); diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index d8cd173183..47d95d8da1 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -159,7 +159,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFileAsync(string name) { var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(Uri, DocumentsContract.GetTreeDocumentId(Uri)); + var treeUri = GetTreeUri().treeUri; var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name); if(newFile == null) { @@ -171,7 +171,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFolderAsync(string name) { - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(Uri, DocumentsContract.GetTreeDocumentId(Uri)); + var treeUri = GetTreeUri().treeUri; var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, DocumentsContract.Document.MimeTypeDir, name); if (newFolder == null) { @@ -207,7 +207,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder } } - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(storageFolder.Uri, DocumentsContract.GetTreeDocumentId(storageFolder.Uri)); + var treeUri = GetTreeUri().treeUri; DocumentsContract.DeleteDocument(Activity.ContentResolver!, treeUri!); } } @@ -230,9 +230,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder yield break; } - var root = PermissionRoot ?? Uri; - var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); - var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); + var (root, childrenUri) = GetTreeUri(); var projection = new[] { @@ -311,9 +309,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return null; } - var root = PermissionRoot ?? Uri; - var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); - var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); + var (root, childrenUri) = GetTreeUri(); var projection = new[] { @@ -370,6 +366,13 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var file = await GetItemAsync(name, false); return (IStorageFile?)file; } + + private (AndroidUri root, AndroidUri? treeUri) GetTreeUri() + { + var root = PermissionRoot ?? Uri; + var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); + return (root, DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId)); + } } internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder @@ -407,7 +410,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF } return isOutput - ? context.ContentResolver?.OpenOutputStream(uri) + ? context.ContentResolver?.OpenOutputStream(uri, "wt") : context.ContentResolver?.OpenInputStream(uri); } diff --git a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs index c1abaa05a5..d8e17e4330 100644 --- a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs +++ b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Vulkan; namespace Avalonia.Android.Platform.Vulkan @@ -24,10 +25,10 @@ namespace Avalonia.Android.Platform.Vulkan internal class VulkanSurfaceFactory : IVulkanKhrSurfacePlatformSurfaceFactory { - public bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, object surface) => + public bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, IPlatformRenderSurface surface) => surface is INativePlatformHandleSurface handle; - public IVulkanKhrSurfacePlatformSurface CreateSurface(IVulkanPlatformGraphicsContext context, object handle) => + public IVulkanKhrSurfacePlatformSurface CreateSurface(IVulkanPlatformGraphicsContext context, IPlatformRenderSurface handle) => new AndroidVulkanSurface((INativePlatformHandleSurface)handle); } diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 99b34e1ff0..f00d835020 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -33,6 +33,7 @@ namespace Avalonia.Animation { _fadeOutAnimation = new Animation { + FillMode = FillMode.Forward, Children = { new KeyFrame() @@ -64,6 +65,7 @@ namespace Avalonia.Animation }; _fadeInAnimation = new Animation { + FillMode = FillMode.Forward, Children = { new KeyFrame() @@ -123,6 +125,16 @@ namespace Avalonia.Animation set => _fadeOutAnimation.Easing = value; } + /// + /// Gets or sets the fill mode applied to both fade animations. + /// Defaults to . + /// + public FillMode FillMode + { + get => _fadeOutAnimation.FillMode; + set => _fadeOutAnimation.FillMode = _fadeInAnimation.FillMode = value; + } + /// public async Task Start(Visual? from, Visual? to, CancellationToken cancellationToken) { @@ -147,9 +159,7 @@ namespace Avalonia.Animation await Task.WhenAll(tasks); if (from != null && !cancellationToken.IsCancellationRequested) - { from.IsVisible = false; - } } /// diff --git a/src/Avalonia.Base/Animation/PageSlide.cs b/src/Avalonia.Base/Animation/PageSlide.cs index 402f57885f..24797a6d80 100644 --- a/src/Avalonia.Base/Animation/PageSlide.cs +++ b/src/Avalonia.Base/Animation/PageSlide.cs @@ -61,6 +61,14 @@ namespace Avalonia.Animation /// public Easing SlideOutEasing { get; set; } = new LinearEasing(); + /// + /// Gets or sets the fill mode applied to both slide animations. + /// Defaults to , which keeps the final transform value after + /// the animation completes and prevents a one-frame flash where the outgoing element snaps + /// back to its original position before IsVisible = false takes effect. + /// + public FillMode FillMode { get; set; } = FillMode.Forward; + /// public virtual async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) { @@ -78,6 +86,7 @@ namespace Avalonia.Animation { var animation = new Animation { + FillMode = FillMode, Easing = SlideOutEasing, Children = { @@ -109,6 +118,7 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + FillMode = FillMode, Easing = SlideInEasing, Children = { @@ -137,10 +147,20 @@ namespace Avalonia.Animation await Task.WhenAll(tasks); - if (from != null && !cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) + return; + + if (from != null) { + // Hide BEFORE resetting transform so there is no single-frame flash + // where the element snaps back to position 0 while still visible. from.IsVisible = false; + if (FillMode != FillMode.None) + from.RenderTransform = null; } + + if (to != null && FillMode != FillMode.None) + to.RenderTransform = null; } /// diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 3303ed276e..3ad488b615 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ namespace Avalonia /// public AvaloniaObject() { - VerifyAccess(); _values = new ValueStore(this); } @@ -109,16 +108,22 @@ namespace Avalonia /// internal string DebugDisplay => GetDebugDisplay(true); + /// + /// Returns the that this + /// is associated with. + /// + public Dispatcher Dispatcher { get; } = Dispatcher.CurrentDispatcher; + /// /// Returns a value indicating whether the current thread is the UI thread. /// /// true if the current thread is the UI thread; otherwise false. - public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); - + public bool CheckAccess() => Dispatcher.CheckAccess(); + /// /// Checks that the current thread is the UI thread and throws if not. /// - public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); + public void VerifyAccess() => Dispatcher.VerifyAccess(); /// /// Clears a 's local value. diff --git a/src/Avalonia.Base/Data/CompiledBinding.cs b/src/Avalonia.Base/Data/CompiledBinding.cs index e243246b4f..ca8ecc62ff 100644 --- a/src/Avalonia.Base/Data/CompiledBinding.cs +++ b/src/Avalonia.Base/Data/CompiledBinding.cs @@ -28,7 +28,7 @@ public class CompiledBinding : BindingBase /// /// The binding path. public CompiledBinding(CompiledBindingPath path) => Path = path; - + /// /// Creates a from a lambda expression. /// @@ -81,12 +81,10 @@ public class CompiledBinding : BindingBase /// Indexers: x => x.Items[0] /// Type casts: x => ((DerivedType)x).Property /// Logical NOT: x => !x.BoolProperty - /// Stream bindings: x => x.TaskProperty (Task/Observable) /// AvaloniaProperty access: x => x[MyProperty] /// /// - [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")] public static CompiledBinding Create( Expression> expression, object? source = null, diff --git a/src/Avalonia.Base/Data/CompiledBindingPath.cs b/src/Avalonia.Base/Data/CompiledBindingPath.cs index 886d89df43..2b8b914122 100644 --- a/src/Avalonia.Base/Data/CompiledBindingPath.cs +++ b/src/Avalonia.Base/Data/CompiledBindingPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; @@ -155,12 +156,26 @@ namespace Avalonia.Data return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder StreamObservable() { _elements.Add(new ObservableStreamPathElement()); return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder Self() { _elements.Add(new SelfPathElement()); @@ -197,6 +212,12 @@ namespace Avalonia.Data return this; } + public CompiledBindingPathBuilder TypeCast(Type targetType) + { + _elements.Add(new TypeCastPathElement(targetType)); + return this; + } + public CompiledBindingPathBuilder TemplatedParent() { _elements.Add(new TemplatedParentPathElement()); @@ -299,6 +320,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + internal class ObservableStreamPathElement : IStronglyTypedStreamElement { public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); @@ -306,6 +335,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement { public static readonly SelfPathElement Instance = new SelfPathElement(); @@ -387,7 +424,28 @@ namespace Avalonia.Data public Type Type => typeof(T); - public Func Cast => TryCast; + public Func Cast { get; } = TryCast; + + public override string ToString() + => $"({Type.FullName})"; + } + + internal class TypeCastPathElement : ITypeCastElement + { + public TypeCastPathElement(Type type) + { + Type = type; + Cast = obj => + { + if (obj is { } result && type.IsInstanceOfType(result)) + return result; + return null; + }; + } + + public Type Type { get; } + + public Func Cast { get; } public override string ToString() => $"({Type.FullName})"; diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 00a90f87fe..81a21955c4 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -460,12 +460,9 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I StopDelayTimer(); if (TryGetTarget(out var target) && - TargetProperty is not null && - target.GetValue(TargetProperty) is var value && - LeafNode is { } leafNode && - !TypeUtilities.IdentityEquals(value, leafNode.Value, TargetType)) + TargetProperty is not null) { - WriteValueToSource(value); + WriteValueToSource(target.GetValue(TargetProperty)); } } diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs index c354bcc15a..8525dd8493 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -8,9 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Avalonia.Data.Core.ExpressionNodes; -using Avalonia.Data.Core.ExpressionNodes.Reflection; using Avalonia.Data.Core.Plugins; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core.Parsers; @@ -25,7 +22,6 @@ namespace Avalonia.Data.Core.Parsers; /// can then be converted into instances. It supports property access, /// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators. /// -[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] internal class BindingExpressionVisitor(LambdaExpression expression) : ExpressionVisitor { @@ -149,17 +145,11 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr instanceType.GetGenericTypeDefinition() == typeof(Task<>) && genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0]))) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamTask()); } - else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type)) + else if (instanceType is not null && ObservableStreamPlugin.MatchesType(instanceType)) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamObservable()); } } else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod) @@ -194,18 +184,12 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr if (!node.Type.IsValueType && !node.Operand.Type.IsValueType && (node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type))) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } } else if (node.NodeType == ExpressionType.TypeAs) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 523c0bcf39..b747542ee5 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -31,7 +31,18 @@ namespace Avalonia.Data.Core.Plugins { reference.TryGetTarget(out var target); - return target != null && target.GetType().GetInterfaces().Any(x => + return target != null && MatchesType(target.GetType()); + } + + public static bool MatchesType(Type type) + { + var interfaces = type.GetInterfaces().AsEnumerable(); + if (type.IsInterface) + { + interfaces = interfaces.Concat([type]); + } + + return interfaces.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 56a7fcefe4..5d395b8b5d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -61,10 +61,9 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] private static IObservable HandleCompleted(Task task) { - var resultProperty = task.GetType().GetRuntimeProperty("Result"); + var resultProperty = GetTaskResult(task); if (resultProperty != null) { @@ -80,6 +79,14 @@ namespace Avalonia.Data.Core.Plugins } return Observable.Empty(); + + [DynamicDependency("Result", typeof(Task<>))] + [UnconditionalSuppressMessage("Trimming", "IL2070")] + [UnconditionalSuppressMessage("Trimming", "IL2075")] + PropertyInfo? GetTaskResult(Task obj) + { + return obj.GetType().GetProperty("Result"); + } } } } diff --git a/src/Avalonia.Controls/ContextRequestedEventArgs.cs b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs similarity index 86% rename from src/Avalonia.Controls/ContextRequestedEventArgs.cs rename to src/Avalonia.Base/Input/ContextRequestedEventArgs.cs index fa0d5f9855..4d8cf333d1 100644 --- a/src/Avalonia.Controls/ContextRequestedEventArgs.cs +++ b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs @@ -1,7 +1,6 @@ -using Avalonia.Input; -using Avalonia.Interactivity; +using Avalonia.Interactivity; -namespace Avalonia.Controls +namespace Avalonia.Input { /// /// Provides event data for the ContextRequested event. @@ -14,7 +13,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the ContextRequestedEventArgs class. /// public ContextRequestedEventArgs() - : base(Control.ContextRequestedEvent) + : base(InputElement.ContextRequestedEvent) { } @@ -34,10 +33,10 @@ namespace Avalonia.Controls } /// - /// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied . + /// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied . /// /// - /// Any -derived object that is connected to the same object tree. + /// Any -derived object that is connected to the same object tree. /// To specify the object relative to the overall coordinate system, use a relativeTo value of null. /// /// @@ -48,7 +47,7 @@ namespace Avalonia.Controls /// /// true if the context request was initiated by a pointer device; otherwise, false. /// - public bool TryGetPosition(Control? relativeTo, out Point point) + public bool TryGetPosition(InputElement? relativeTo, out Point point) { if (_pointerEventArgs is null) { diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 2de2f12aff..21726898b5 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -56,7 +56,7 @@ namespace Avalonia.Input } public Cursor(Bitmap cursor, PixelPoint hotSpot) - : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") + : this(GetCursorFactory().CreateCursor(cursor, hotSpot), "BitmapCursor") { } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs new file mode 100644 index 0000000000..5d17940c8a --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs @@ -0,0 +1,192 @@ +using System; +using Avalonia.Logging; +using Avalonia.Media; + +namespace Avalonia.Input.GestureRecognizers +{ + /// + /// A gesture recognizer that detects swipe gestures and raises + /// on the target element when a swipe is confirmed. + /// + public class SwipeGestureRecognizer : GestureRecognizer + { + private IPointer? _tracking; + private IPointer? _captured; + private Point _initialPosition; + private int _gestureId; + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThresholdProperty = + AvaloniaProperty.Register(nameof(Threshold), 30d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CrossAxisCancelThresholdProperty = + AvaloniaProperty.Register( + nameof(CrossAxisCancelThreshold), 8d); + + /// + /// Defines the property. + /// Leading-edge start zone in px. 0 (default) = full area. + /// When > 0, only starts tracking if the pointer is within this many px + /// of the leading edge (LTR: left; RTL: right). + /// + public static readonly StyledProperty EdgeSizeProperty = + AvaloniaProperty.Register(nameof(EdgeSize), 0d); + + /// + /// Defines the property. + /// When false, the recognizer ignores all pointer events. + /// Lets callers toggle the recognizer at runtime without needing to remove it from the + /// collection (GestureRecognizerCollection has Add but no Remove). + /// Default: true. + /// + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), true); + + /// + /// Gets or sets the minimum distance in pixels the pointer must travel before a swipe + /// is recognized. Default is 30px. + /// + public double Threshold + { + get => GetValue(ThresholdProperty); + set => SetValue(ThresholdProperty, value); + } + + /// + /// Gets or sets the maximum cross-axis drift in pixels allowed before the gesture is + /// cancelled. Default is 8px. + /// + public double CrossAxisCancelThreshold + { + get => GetValue(CrossAxisCancelThresholdProperty); + set => SetValue(CrossAxisCancelThresholdProperty, value); + } + + /// + /// Gets or sets the leading-edge start zone in pixels. When greater than zero, tracking + /// only begins if the pointer is within this distance of the leading edge. Default is 0 + /// (full area). + /// + public double EdgeSize + { + get => GetValue(EdgeSizeProperty); + set => SetValue(EdgeSizeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the recognizer responds to pointer events. + /// Setting this to false is a lightweight alternative to removing the recognizer from + /// the collection. Default is true. + /// + public bool IsEnabled + { + get => GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + protected override void PointerPressed(PointerPressedEventArgs e) + { + if (!IsEnabled) return; + if (!e.GetCurrentPoint(null).Properties.IsLeftButtonPressed) return; + if (Target is not Visual visual) return; + + var pos = e.GetPosition(visual); + var edgeSize = EdgeSize; + + if (edgeSize > 0) + { + bool isRtl = visual.FlowDirection == FlowDirection.RightToLeft; + bool inEdge = isRtl + ? pos.X >= visual.Bounds.Width - edgeSize + : pos.X <= edgeSize; + if (!inEdge) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log( + this, "SwipeGestureRecognizer: press at {Pos} outside edge zone ({EdgeSize}px), ignoring", + pos, edgeSize); + return; + } + } + + _gestureId = SwipeGestureEventArgs.GetNextFreeId(); + _tracking = e.Pointer; + _initialPosition = pos; + + Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log( + this, "SwipeGestureRecognizer: tracking started at {Pos} (pointer={PointerType})", + pos, e.Pointer.Type); + } + + protected override void PointerMoved(PointerEventArgs e) + { + if (_tracking != e.Pointer || Target is not Visual visual) return; + + var pos = e.GetPosition(visual); + double dx = pos.X - _initialPosition.X; + double dy = pos.Y - _initialPosition.Y; + double absDx = Math.Abs(dx); + double absDy = Math.Abs(dy); + double threshold = Threshold; + + if (absDx < threshold && absDy < threshold) + return; + + SwipeDirection dir; + Vector delta; + if (absDx >= absDy) + { + dir = dx > 0 ? SwipeDirection.Right : SwipeDirection.Left; + delta = new Vector(dx, 0); + } + else + { + dir = dy > 0 ? SwipeDirection.Down : SwipeDirection.Up; + delta = new Vector(0, dy); + } + + Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log( + this, "SwipeGestureRecognizer: swipe recognized — direction={Direction}, delta={Delta}", + dir, delta); + + _tracking = null; + _captured = e.Pointer; + Capture(e.Pointer); + e.Handled = true; + + var args = new SwipeGestureEventArgs(_gestureId, dir, delta, _initialPosition); + Target?.RaiseEvent(args); + } + + protected override void PointerReleased(PointerReleasedEventArgs e) + { + if (_tracking == e.Pointer) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log( + this, "SwipeGestureRecognizer: pointer released without crossing threshold — gesture discarded"); + _tracking = null; + } + + if (_captured == e.Pointer) + { + (e.Pointer as Pointer)?.CaptureGestureRecognizer(null); + _captured = null; + } + } + + protected override void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log( + this, "SwipeGestureRecognizer: capture lost — gesture cancelled"); + _tracking = null; + } + _captured = null; + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 4929cd0ea6..3ae504a77f 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -1,15 +1,23 @@ using System; using System.Threading; using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.Threading; using Avalonia.Reactive; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Input { - public static class Gestures + internal static class Gestures { + // These events are only used internally and not propagated through a route. + // They have routed event args as their target type because their args generated from + // PointerEventArgs. In order to prevent having identical classes with just their base class + // being different, these events use routed args. + public static event EventHandler? Holding; + public static event EventHandler? Tapped; + public static event EventHandler? RightTapped; + public static event EventHandler? DoubleTapped; + private record struct GestureState(GestureStateType Type, IPointer Pointer); private enum GestureStateType { @@ -23,98 +31,6 @@ namespace Avalonia.Input private static Point s_lastPressPoint; private static CancellationTokenSource? s_holdCancellationToken; - /// - /// Defines the IsHoldingEnabled attached property. - /// - public static readonly AttachedProperty IsHoldingEnabledProperty = - AvaloniaProperty.RegisterAttached("IsHoldingEnabled", typeof(Gestures), true); - - /// - /// Defines the IsHoldWithMouseEnabled attached property. - /// - public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = - AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(Gestures), false); - - public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( - "Tapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( - "DoubleTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( - "RightTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEvent = - RoutedEvent.Register( - "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = - RoutedEvent.Register( - "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerTouchPadGestureMagnify", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerTouchPadGestureRotate", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerTouchPadGestureSwipe", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PinchEvent = - RoutedEvent.Register( - "Pinch", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PinchEndedEvent = - RoutedEvent.Register( - "PinchEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PullGestureEvent = - RoutedEvent.Register( - "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - /// - /// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact). - /// - public static readonly RoutedEvent HoldingEvent = - RoutedEvent.Register( - "Holding", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PullGestureEndedEvent = - RoutedEvent.Register( - "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static bool GetIsHoldingEnabled(StyledElement element) - { - return element.GetValue(IsHoldingEnabledProperty); - } - public static void SetIsHoldingEnabled(StyledElement element, bool value) - { - element.SetValue(IsHoldingEnabledProperty, value); - } - - public static bool GetIsHoldWithMouseEnabled(StyledElement element) - { - return element.GetValue(IsHoldWithMouseEnabledProperty); - } - public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) - { - element.SetValue(IsHoldWithMouseEnabledProperty, value); - } - static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); @@ -122,102 +38,6 @@ namespace Avalonia.Input InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved); } - public static void AddTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(TappedEvent, handler); - } - - public static void AddDoubleTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(DoubleTappedEvent, handler); - } - - public static void AddRightTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(RightTappedEvent, handler); - } - - public static void AddHoldingHandler(Interactive element, EventHandler handler) => - element.AddHandler(HoldingEvent, handler); - - public static void AddPinchHandler(Interactive element, EventHandler handler) => - element.AddHandler(PinchEvent, handler); - - public static void AddPinchEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(PinchEndedEvent, handler); - - public static void AddPullGestureHandler(Interactive element, EventHandler handler) => - element.AddHandler(PullGestureEvent, handler); - - public static void AddPullGestureEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(PullGestureEndedEvent, handler); - - public static void AddPointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureMagnifyEvent, handler); - - public static void AddPointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureRotateEvent, handler); - - public static void AddPointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureSwipeEvent, handler); - - public static void AddScrollGestureHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEvent, handler); - - public static void AddScrollGestureEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEndedEvent, handler); - - public static void AddScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureInertiaStartingEvent, handler); - - public static void RemoveTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(TappedEvent, handler); - } - - public static void RemoveDoubleTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(DoubleTappedEvent, handler); - } - - public static void RemoveRightTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(RightTappedEvent, handler); - } - - public static void RemoveHoldingHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(HoldingEvent, handler); - - public static void RemovePinchHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PinchEvent, handler); - - public static void RemovePinchEndedHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PinchEndedEvent, handler); - - public static void RemovePullGestureHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PullGestureEvent, handler); - - public static void RemovePullGestureEndedHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PullGestureEndedEvent, handler); - - public static void RemovePointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureMagnifyEvent, handler); - - public static void RemovePointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureRotateEvent, handler); - - public static void RemovePointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureSwipeEvent, handler); - - public static void RemoveScrollGestureHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureEvent,handler); - - public static void RemoveScrollGestureEndedHandler(Interactive element,EventHandler handler) => - element.RemoveHandler(ScrollGestureEndedEvent,handler); - - public static void RemoveScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureInertiaStartingEvent, handler); - private static object? GetCaptured(RoutedEventArgs? args) { if (args is not PointerEventArgs pointerEventArgs) @@ -237,11 +57,11 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (Visual)source; - if(s_gestureState != null) + if (s_gestureState != null) { - if(s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i) + if (s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Canceled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } s_holdCancellationToken?.Cancel(); s_holdCancellationToken?.Dispose(); @@ -263,22 +83,23 @@ namespace Avalonia.Input { DispatcherTimer.RunOnce(() => { - if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i))) + if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && InputElement.GetIsHoldingEnabled(i) && + (e.Pointer.Type != PointerType.Mouse || InputElement.GetIsHoldWithMouseEnabled(i))) { s_gestureState = new GestureState(GestureStateType.Holding, s_gestureState.Value.Pointer); - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } }, settings.HoldWaitDuration); } } else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - if (s_lastPress.TryGetTarget(out var target) && - target == source && + if (s_lastPress.TryGetTarget(out var target) && + target == source && source is Interactive i) { s_gestureState = new GestureState(GestureStateType.DoubleTapped, e.Pointer); - i.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); + DoubleTapped?.Invoke(i, new TappedEventArgs(InputElement.DoubleTappedEvent, e)); } } } @@ -307,17 +128,19 @@ namespace Avalonia.Input { if (s_gestureState?.Type == GestureStateType.Holding) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + + RightTapped?.Invoke(i, new TappedEventArgs(InputElement.RightTappedEvent, e)); } else if (e.InitialPressMouseButton == MouseButton.Right) { - i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); + RightTapped?.Invoke(i, new TappedEventArgs(InputElement.RightTappedEvent, e)); } //GestureStateType.DoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. //This behaviour matches UWP behaviour. else if (s_gestureState?.Type != GestureStateType.DoubleTapped) { - i.RaiseEvent(new TappedEventArgs(TappedEvent, e)); + Tapped?.Invoke(i, new TappedEventArgs(InputElement.TappedEvent, e)); } } s_gestureState = null; @@ -351,15 +174,15 @@ namespace Avalonia.Input if (s_gestureState.Value.Type == GestureStateType.Holding) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Canceled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } + + s_holdCancellationToken?.Cancel(); + s_holdCancellationToken?.Dispose(); + s_holdCancellationToken = null; + s_gestureState = null; } } - - s_holdCancellationToken?.Cancel(); - s_holdCancellationToken?.Dispose(); - s_holdCancellationToken = null; - s_gestureState = null; } } } diff --git a/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs index efb0c01599..ffc5dfec70 100644 --- a/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs +++ b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs @@ -6,7 +6,7 @@ namespace Avalonia.Input public class HoldingRoutedEventArgs : RoutedEventArgs { /// - /// Gets the state of the event. + /// Gets the state of the event. /// public HoldingState HoldingState { get; } @@ -20,25 +20,18 @@ namespace Avalonia.Input /// public PointerType PointerType { get; } - internal PointerEventArgs? PointerEventArgs { get; } + internal PointerEventArgs PointerEventArgs { get; } /// /// Initializes a new instance of the class. /// - public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent) + internal HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType, PointerEventArgs pointerEventArgs) : base(InputElement.HoldingEvent) { + PointerEventArgs = pointerEventArgs; HoldingState = holdingState; Position = position; PointerType = pointerType; } - - /// - /// Initializes a new instance of the class. - /// - internal HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType, PointerEventArgs pointerEventArgs) : this(holdingState, position, pointerType) - { - PointerEventArgs = pointerEventArgs; - } } public enum HoldingState @@ -56,6 +49,6 @@ namespace Avalonia.Input /// /// An additional contact is detected or a subsequent gesture (such as a slide) is detected. /// - Cancelled, + Canceled, } } diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs index 513ecb0fae..13b7bae813 100644 --- a/src/Avalonia.Base/Input/IInputRoot.cs +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -33,5 +33,17 @@ namespace Avalonia.Input // It's also currently used by automation since we have special WindowAutomationPeer which needs to target the // window itself public InputElement FocusRoot { get; } + + /// + /// Performs a hit-test for chrome/decoration elements at the given position. + /// + /// The point in root-relative coordinates. + /// + /// null if no chrome element was hit (no chrome involvement at this point). + /// or + /// if an interactive chrome element was hit — the platform should redirect non-client input to regular client input. + /// Any other non- value indicates a specific non-client role (titlebar, resize grip, etc.). + /// + internal WindowDecorationsElementRole? HitTestChromeElement(Point point) => null; } } diff --git a/src/Avalonia.Base/Input/InputElement.Gestures.cs b/src/Avalonia.Base/Input/InputElement.Gestures.cs new file mode 100644 index 0000000000..1ad1146282 --- /dev/null +++ b/src/Avalonia.Base/Input/InputElement.Gestures.cs @@ -0,0 +1,319 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public partial class InputElement + { + private bool _isContextMenuOnHolding; + + /// + /// Defines the IsHoldingEnabled attached property. + /// + public static readonly AttachedProperty IsHoldingEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldingEnabled", typeof(InputElement), true); + + /// + /// Defines the IsHoldWithMouseEnabled attached property. + /// + public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(InputElement), false); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PinchEvent = + RoutedEvent.Register( + nameof(Pinch), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PinchEndedEvent = + RoutedEvent.Register( + nameof(PinchEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + nameof(PullGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + nameof(PullGestureEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SwipeGestureEvent = + RoutedEvent.Register( + nameof(SwipeGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + nameof(ScrollGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = + RoutedEvent.Register( + nameof(ScrollGestureInertiaStarting), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + nameof(ScrollGestureEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureMagnify), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureRotate), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureSwipe), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = + RoutedEvent.Register( + nameof(Tapped), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RightTappedEvent = + RoutedEvent.Register( + nameof(RightTapped), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HoldingEvent = + RoutedEvent.Register( + nameof(Holding), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = + RoutedEvent.Register( + nameof(DoubleTapped), + RoutingStrategies.Bubble); + + public static bool GetIsHoldingEnabled(StyledElement element) + { + return element.GetValue(IsHoldingEnabledProperty); + } + + public static void SetIsHoldingEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldingEnabledProperty, value); + } + + public static bool GetIsHoldWithMouseEnabled(StyledElement element) + { + return element.GetValue(IsHoldWithMouseEnabledProperty); + } + + public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldWithMouseEnabledProperty, value); + } + + /// + /// Occurs when a pinch gesture occurs on the control. + /// + public event EventHandler? Pinch + { + add { AddHandler(PinchEvent, value); } + remove { RemoveHandler(PinchEvent, value); } + } + + /// + /// Occurs when a pinch gesture ends on the control. + /// + public event EventHandler? PinchEnded + { + add { AddHandler(PinchEndedEvent, value); } + remove { RemoveHandler(PinchEndedEvent, value); } + } + + /// + /// Occurs when a pull gesture occurs on the control. + /// + public event EventHandler? PullGesture + { + add { AddHandler(PullGestureEvent, value); } + remove { RemoveHandler(PullGestureEvent, value); } + } + + /// + /// Occurs when a pull gesture ends on the control. + /// + public event EventHandler? PullGestureEnded + { + add { AddHandler(PullGestureEndedEvent, value); } + remove { RemoveHandler(PullGestureEndedEvent, value); } + } + + /// + /// Occurs when a scroll gesture occurs on the control. + /// + public event EventHandler? ScrollGesture + { + add { AddHandler(ScrollGestureEvent, value); } + remove { RemoveHandler(ScrollGestureEvent, value); } + } + + /// + /// Occurs when a scroll gesture inertia starts on the control. + /// + public event EventHandler? ScrollGestureInertiaStarting + { + add { AddHandler(ScrollGestureInertiaStartingEvent, value); } + remove { RemoveHandler(ScrollGestureInertiaStartingEvent, value); } + } + + /// + /// Occurs when a scroll gesture ends on the control. + /// + public event EventHandler? ScrollGestureEnded + { + add { AddHandler(ScrollGestureEndedEvent, value); } + remove { RemoveHandler(ScrollGestureEndedEvent, value); } + } + + /// + /// Occurs when a touchpad magnify gesture occurs on the control. + /// + public event EventHandler? PointerTouchPadGestureMagnify + { + add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } + } + + /// + /// Occurs when a touchpad rotate gesture occurs on the control. + /// + public event EventHandler? PointerTouchPadGestureRotate + { + add { AddHandler(PointerTouchPadGestureRotateEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } + } + + /// + /// Occurs when a swipe gesture occurs on the control. + /// + public event EventHandler? SwipeGesture + { + add { AddHandler(SwipeGestureEvent, value); } + remove { RemoveHandler(SwipeGestureEvent, value); } + } + + /// + /// Occurs when a touchpad swipe gesture occurs on the control. + /// + public event EventHandler? PointerTouchPadGestureSwipe + { + add { AddHandler(PointerTouchPadGestureSwipeEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureSwipeEvent, value); } + } + + /// + /// Occurs when a tap gesture occurs on the control. + /// + public event EventHandler? Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// Occurs when a right tap gesture occurs on the control. + /// + public event EventHandler? RightTapped + { + add { AddHandler(RightTappedEvent, value); } + remove { RemoveHandler(RightTappedEvent, value); } + } + + /// + /// Occurs when a hold gesture occurs on the control. + /// + public event EventHandler? Holding + { + add { AddHandler(HoldingEvent, value); } + remove { RemoveHandler(HoldingEvent, value); } + } + + /// + /// Occurs when a double-tap gesture occurs on the control. + /// + public event EventHandler? DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + + private static void OnPreviewHolding(object? sender, HoldingRoutedEventArgs e) + { + if (sender is InputElement inputElement) + { + inputElement.RaiseEvent(e); + + if (!e.Handled && e.HoldingState == HoldingState.Started) + { + var contextEvent = new ContextRequestedEventArgs(e.PointerEventArgs); + inputElement.RaiseEvent(contextEvent); + e.Handled = contextEvent.Handled; + + if (contextEvent.Handled) + { + inputElement._isContextMenuOnHolding = true; + } + } + else if (e.HoldingState == HoldingState.Canceled && inputElement._isContextMenuOnHolding) + { + inputElement.RaiseEvent(new RoutedEventArgs(InputElement.ContextCanceledEvent) + { + Source = inputElement + }); + + inputElement._isContextMenuOnHolding = false; + } + else if (e.HoldingState == HoldingState.Completed) + { + inputElement._isContextMenuOnHolding = false; + } + } + } + } +} diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index f929dcbe05..98b988f8cd 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -16,7 +16,7 @@ namespace Avalonia.Input /// Implements input-related functionality for a control. /// [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] - public class InputElement : Interactive, IInputElement + public partial class InputElement : Interactive, IInputElement { /// /// Defines the property. @@ -203,24 +203,20 @@ namespace Avalonia.Input RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// - /// Defines the event. + /// Provides event data for the event. /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent RightTappedEvent = Gestures.RightTappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent HoldingEvent = Gestures.HoldingEvent; + public static readonly RoutedEvent ContextRequestedEvent = + RoutedEvent.Register( + nameof(ContextRequested), + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// - /// Defines the event. + /// Provides event data for the event. /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + public static readonly RoutedEvent ContextCanceledEvent = + RoutedEvent.Register( + nameof(ContextCanceled), + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); private bool _isEffectivelyEnabled = true; private bool _isFocused; @@ -257,6 +253,12 @@ namespace Avalonia.Input DoubleTappedEvent.AddClassHandler((x, e) => x.OnDoubleTapped(e)); HoldingEvent.AddClassHandler((x, e) => x.OnHolding(e)); + Gestures.Tapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + Gestures.RightTapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + Gestures.DoubleTapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + + Gestures.Holding += OnPreviewHolding; + // Gesture only handlers PointerMovedEvent.AddClassHandler((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true); PointerPressedEvent.AddClassHandler((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true); @@ -419,42 +421,6 @@ namespace Avalonia.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } - /// - /// Occurs when a tap gesture occurs on the control. - /// - public event EventHandler? Tapped - { - add { AddHandler(TappedEvent, value); } - remove { RemoveHandler(TappedEvent, value); } - } - - /// - /// Occurs when a right tap gesture occurs on the control. - /// - public event EventHandler? RightTapped - { - add { AddHandler(RightTappedEvent, value); } - remove { RemoveHandler(RightTappedEvent, value); } - } - - /// - /// Occurs when a hold gesture occurs on the control. - /// - public event EventHandler? Holding - { - add { AddHandler(HoldingEvent, value); } - remove { RemoveHandler(HoldingEvent, value); } - } - - /// - /// Occurs when a double-tap gesture occurs on the control. - /// - public event EventHandler? DoubleTapped - { - add { AddHandler(DoubleTappedEvent, value); } - remove { RemoveHandler(DoubleTappedEvent, value); } - } - /// /// Gets or sets a value indicating whether the control can receive focus. /// @@ -518,6 +484,24 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } + /// + /// Occurs when the user has completed a context input gesture, such as a right-click. + /// + public event EventHandler? ContextRequested + { + add => AddHandler(ContextRequestedEvent, value); + remove => RemoveHandler(ContextRequestedEvent, value); + } + + /// + /// Occurs when the context input gesture continues into another gesture, to notify the element that the context flyout should not be opened. + /// + public event EventHandler? ContextCanceled + { + add => AddHandler(ContextCanceledEvent, value); + remove => RemoveHandler(ContextCanceledEvent, value); + } + /// /// Gets or sets a value that indicates whether the control is included in tab navigation. /// diff --git a/src/Avalonia.Base/Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs index c9b1751b2a..7f5b5f82e7 100644 --- a/src/Avalonia.Base/Input/InputManager.cs +++ b/src/Avalonia.Base/Input/InputManager.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// Receives input from the windowing subsystem and dispatches it to interested parties /// for processing. /// - internal class InputManager : IInputManager + internal class InputManager : IInputManager, IDisposable { private readonly LightweightSubject _preProcess = new(); private readonly LightweightSubject _process = new(); @@ -36,5 +36,12 @@ namespace Avalonia.Input _process.OnNext(e); _postProcess.OnNext(e); } + + public void Dispose() + { + _preProcess.OnCompleted(); + _process.OnCompleted(); + _postProcess.OnCompleted(); + } } } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index d9766c1707..2d112efda1 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using Avalonia.Reactive; +using Avalonia.Input.GestureRecognizers; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.Utilities; using Avalonia.VisualTree; -using Avalonia.Input.GestureRecognizers; #pragma warning disable CS0618 namespace Avalonia.Input @@ -263,7 +260,7 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureMagnifyEvent, source, _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); @@ -283,7 +280,7 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureRotateEvent, source, _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); @@ -303,7 +300,7 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureSwipeEvent, source, _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); diff --git a/src/Avalonia.Base/Input/PinchEventArgs.cs b/src/Avalonia.Base/Input/PinchEventArgs.cs index be373f443a..4c037f30d7 100644 --- a/src/Avalonia.Base/Input/PinchEventArgs.cs +++ b/src/Avalonia.Base/Input/PinchEventArgs.cs @@ -4,13 +4,13 @@ namespace Avalonia.Input { public class PinchEventArgs : RoutedEventArgs { - public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent) + public PinchEventArgs(double scale, Point scaleOrigin) : base(InputElement.PinchEvent) { Scale = scale; ScaleOrigin = scaleOrigin; } - public PinchEventArgs(double scale, Point scaleOrigin, double angle, double angleDelta) : base(Gestures.PinchEvent) + public PinchEventArgs(double scale, Point scaleOrigin, double angle, double angleDelta) : base(InputElement.PinchEvent) { Scale = scale; ScaleOrigin = scaleOrigin; @@ -41,7 +41,7 @@ namespace Avalonia.Input public class PinchEndedEventArgs : RoutedEventArgs { - public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent) + public PinchEndedEventArgs() : base(InputElement.PinchEndedEvent) { } } diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs index 34d95c87f4..14b78f8730 100644 --- a/src/Avalonia.Base/Input/PullGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/PullGestureEventArgs.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Interactivity; namespace Avalonia.Input @@ -12,8 +11,8 @@ namespace Avalonia.Input private static int _nextId = 1; internal static int GetNextFreeId() => _nextId++; - - public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) + + public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(InputElement.PullGestureEvent) { Id = id; Delta = delta; @@ -26,7 +25,7 @@ namespace Avalonia.Input public int Id { get; } public PullDirection PullDirection { get; } - public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) + public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(InputElement.PullGestureEndedEvent) { Id = id; PullDirection = pullDirection; diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index ac770fd0a2..150ac17226 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input public static int GetNextFreeId() => _nextId++; - public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + public ScrollGestureEventArgs(int id, Vector delta) : base(InputElement.ScrollGestureEvent) { Id = id; Delta = delta; @@ -25,7 +25,7 @@ namespace Avalonia.Input { public int Id { get; } - public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + public ScrollGestureEndedEventArgs(int id) : base(InputElement.ScrollGestureEndedEvent) { Id = id; } @@ -36,7 +36,7 @@ namespace Avalonia.Input public int Id { get; } public Vector Inertia { get; } - internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent) + internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(InputElement.ScrollGestureInertiaStartingEvent) { Id = id; Inertia = inertia; diff --git a/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs b/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs new file mode 100644 index 0000000000..0c2a91556a --- /dev/null +++ b/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs @@ -0,0 +1,50 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + /// + /// Specifies the direction of a swipe gesture. + /// + public enum SwipeDirection { Left, Right, Up, Down } + + /// + /// Provides data for the routed event. + /// + public class SwipeGestureEventArgs : RoutedEventArgs + { + private static int _nextId = 1; + internal static int GetNextFreeId() => _nextId++; + + /// + /// Gets the unique identifier for this swipe gesture instance. + /// + public int Id { get; } + + /// + /// Gets the direction of the swipe gesture. + /// + public SwipeDirection SwipeDirection { get; } + + /// + /// Gets the total translation vector of the swipe gesture. + /// + public Vector Delta { get; } + + /// + /// Gets the position, relative to the target element, where the swipe started. + /// + public Point StartPoint { get; } + + /// + /// Initializes a new instance of . + /// + public SwipeGestureEventArgs(int id, SwipeDirection direction, Vector delta, Point startPoint) + : base(InputElement.SwipeGestureEvent) + { + Id = id; + SwipeDirection = direction; + Delta = delta; + StartPoint = startPoint; + } + } +} diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs index eaffa1d8bc..a235d495d6 100644 --- a/src/Avalonia.Base/Input/TappedEventArgs.cs +++ b/src/Avalonia.Base/Input/TappedEventArgs.cs @@ -1,6 +1,4 @@ -using System; using Avalonia.Interactivity; -using Avalonia.VisualTree; namespace Avalonia.Input { @@ -17,7 +15,7 @@ namespace Avalonia.Input public IPointer Pointer => lastPointerEventArgs.Pointer; public KeyModifiers KeyModifiers => lastPointerEventArgs.KeyModifiers; public ulong Timestamp => lastPointerEventArgs.Timestamp; - + public Point GetPosition(Visual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo); } } diff --git a/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs new file mode 100644 index 0000000000..faa0922c33 --- /dev/null +++ b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs @@ -0,0 +1,100 @@ +namespace Avalonia.Input; + +/// +/// Defines the cross-platform role of a visual element for non-client hit-testing. +/// Used to mark elements as titlebar drag areas, resize grips, etc. +/// +public enum WindowDecorationsElementRole +{ + /// + /// No special role. The element is invisible to chrome hit-testing. + /// + None, + + /// + /// An interactive element that is part of the decorations chrome (e.g., a caption button). + /// Set by themes on decoration template elements. Input is passed through to the element + /// rather than being intercepted for non-client actions. + /// + DecorationsElement, + + /// + /// An interactive element set by user code that should receive input even when + /// overlapping chrome areas. Has the same effect as + /// but is intended for use by application developers. + /// + User, + + /// + /// The element acts as a titlebar drag area. + /// Clicking and dragging on this element initiates a platform window move. + /// + TitleBar, + + /// + /// Resize grip for the north (top) edge. + /// + ResizeN, + + /// + /// Resize grip for the south (bottom) edge. + /// + ResizeS, + + /// + /// Resize grip for the east (right) edge. + /// + ResizeE, + + /// + /// Resize grip for the west (left) edge. + /// + ResizeW, + + /// + /// Resize grip for the northeast corner. + /// + ResizeNE, + + /// + /// Resize grip for the northwest corner. + /// + ResizeNW, + + /// + /// Resize grip for the southeast corner. + /// + ResizeSE, + + /// + /// Resize grip for the southwest corner. + /// + ResizeSW, + + /// + /// The element acts as the window close button. + /// On Win32, maps to HTCLOSE for system close behavior. + /// On other platforms, treated as an interactive decoration element. + /// + CloseButton, + + /// + /// The element acts as the window minimize button. + /// On Win32, maps to HTMINBUTTON for system minimize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MinimizeButton, + + /// + /// The element acts as the window maximize/restore button. + /// On Win32, maps to HTMAXBUTTON for system maximize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MaximizeButton, + + /// + /// The element acts as the window fullscreen toggle button. + /// Treated as an interactive decoration element on all platforms. + /// + FullScreenButton +} diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index 84f1fc4bcc..c50053dc05 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -147,8 +147,7 @@ namespace Avalonia.Layout /// coordinates by rounding the size up to the nearest pixel. /// /// Input size. - /// DPI along x-dimension. - /// DPI along y-dimension. + /// The DPI scale. /// Value of size that will be rounded under screen DPI. /// /// This is a layout helper method. It takes DPI into account and also does not return @@ -156,14 +155,8 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// - public static Size RoundLayoutSizeUp(Size size, double dpiScaleX, double dpiScaleY) - { - return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY)); - } - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Size RoundLayoutSizeUp(Size size, double dpiScale) + public static Size RoundLayoutSizeUp(Size size, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -180,8 +173,7 @@ namespace Avalonia.Layout /// coordinates. /// /// Input thickness. - /// DPI along x-dimension. - /// DPI along y-dimension. + /// The DPI scale. /// Value of thickness that will be rounded under screen DPI. /// /// This is a layout helper method. It takes DPI into account and also does not return @@ -189,19 +181,8 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// - public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY) - { - return new Thickness( - RoundLayoutValue(thickness.Left, dpiScaleX), - RoundLayoutValue(thickness.Top, dpiScaleY), - RoundLayoutValue(thickness.Right, dpiScaleX), - RoundLayoutValue(thickness.Bottom, dpiScaleY) - ); - } - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale) + public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -219,7 +200,7 @@ namespace Avalonia.Layout [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Point RoundLayoutPoint(Point point, double dpiScale) + public static Point RoundLayoutPoint(Point point, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -244,10 +225,11 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] public static double RoundLayoutValue(double value, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. - return MathUtilities.IsOne(dpiScale) ? + return dpiScale == 1.0 ? Math.Round(value) : Math.Round(value * dpiScale) / dpiScale; } @@ -265,10 +247,11 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] public static double RoundLayoutValueUp(double value, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. - return MathUtilities.IsOne(dpiScale) ? + return dpiScale == 1.0 ? Math.Ceiling(value) : Math.Ceiling(RoundTo8Digits(value) * dpiScale) / dpiScale; } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index e2076d34b6..40176c88ff 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -731,7 +731,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch + i < 9; i++) { - if (TryGetWithStretch(stretch, out glyphTypeface)) + if (TryGetWithStretch(stretch + i, out glyphTypeface)) { return true; } @@ -741,7 +741,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch - i > 1; i++) { - if (TryGetWithStretch(stretch, out glyphTypeface)) + if (TryGetWithStretch(stretch - i, out glyphTypeface)) { return true; } @@ -786,7 +786,7 @@ namespace Avalonia.Media.Fonts //Look for available weights between the target and 500, in ascending order. for (var i = 0; weight + i <= 500; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -795,7 +795,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } @@ -804,7 +804,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights greater than 500, in ascending order. for (var i = 0; weight + i <= 900; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -816,7 +816,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight - i >= 100; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } @@ -825,7 +825,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight + i <= 900; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -837,7 +837,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight + i <= 900; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -846,7 +846,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (TryGetWithWeight(weight, out glyphTypeface)) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } diff --git a/src/Avalonia.Base/Media/GlyphTypeface.cs b/src/Avalonia.Base/Media/GlyphTypeface.cs index cb554f3e22..fdbd56947e 100644 --- a/src/Avalonia.Base/Media/GlyphTypeface.cs +++ b/src/Avalonia.Base/Media/GlyphTypeface.cs @@ -122,10 +122,11 @@ namespace Avalonia.Media var isFixedPitch = postTable.IsFixedPitch; var underlineOffset = postTable.UnderlinePosition; var underlineSize = postTable.UnderlineThickness; + var designEmHeight = GetFontDesignEmHeight(headTable); Metrics = new FontMetrics { - DesignEmHeight = headTable?.UnitsPerEm ?? 0, + DesignEmHeight = designEmHeight, Ascent = ascent, Descent = descent, LineGap = lineGap, @@ -225,6 +226,18 @@ namespace Avalonia.Media } } + private static ushort GetFontDesignEmHeight(HeadTable? headTable) + { + var unitsPerEm = headTable?.UnitsPerEm ?? 0; + + // Bitmap fonts may specify 0 or miss the head table completely. + // Use 2048 as sensible default (used by most fonts). + if (unitsPerEm == 0) + unitsPerEm = 2048; + + return unitsPerEm; + } + internal static GlyphTypeface? TryCreate(IPlatformTypeface typeface, FontSimulations fontSimulations = FontSimulations.None) { try diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 63ec02737f..dc1541414b 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -178,12 +178,8 @@ namespace Avalonia.Media.Imaging public virtual AlphaFormat? AlphaFormat => (PlatformImpl.Item as IReadableBitmapImpl)?.AlphaFormat; - private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, - ILockedFramebuffer fb) + private PixelRect ValidateSourceRect(PixelRect sourceRect) { - if (Format == null) - throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); - if ((sourceRect.Width <= 0 || sourceRect.Height <= 0) && (sourceRect.X != 0 || sourceRect.Y != 0)) throw new ArgumentOutOfRangeException(nameof(sourceRect)); @@ -197,6 +193,16 @@ namespace Avalonia.Media.Imaging if (sourceRect.Right > PixelSize.Width || sourceRect.Bottom > PixelSize.Height) throw new ArgumentOutOfRangeException(nameof(sourceRect)); + return sourceRect; + } + + private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, + ILockedFramebuffer fb) + { + if (Format == null) + throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + + sourceRect = ValidateSourceRect(sourceRect); int minStride = checked(((sourceRect.Width * fb.Format.BitsPerPixel) + 7) / 8); if (stride < minStride) @@ -223,8 +229,10 @@ namespace Avalonia.Media.Imaging || PlatformImpl.Item is not IReadableBitmapImpl readable || Format != readable.Format ) + { throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); - + } + if (_isTranscoded) throw new NotSupportedException("CopyPixels is not supported for transcoded bitmaps"); @@ -241,7 +249,13 @@ namespace Avalonia.Media.Imaging { if (PlatformImpl.Item is not IReadableBitmapImpl readable || readable.Format == null || readable.AlphaFormat == null) { - throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + // Since we can't read pixels from the bitmap, we need to render it to a compatible bitmap and read pixels from it. + using var rtb = new RenderTargetBitmap(PixelSize); + using (var ctx = rtb.CreateDrawingContext()) + ctx.DrawImage(this, new Rect(rtb.Size)); + rtb.CopyPixels(buffer); + + return; } if (buffer.Format != readable.Format || buffer.AlphaFormat != readable.AlphaFormat) diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 26229b5ecb..98f90c7768 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging /// The drawing context. public DrawingContext CreateDrawingContext(bool clear) { - var platform = PlatformImpl.Item.CreateDrawingContext(true); + var platform = PlatformImpl.Item.CreateDrawingContext(); if(clear) platform.Clear(Colors.Transparent); return new PlatformDrawingContext(platform); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 24e9019084..802d6ebb9d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -627,7 +627,6 @@ namespace Avalonia.Media.TextFormatting finally { objectPool.TextLines.Return(ref textLines); - objectPool.VerifyAllReturned(); } } diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs index 8246d4a18d..9685ed3817 100644 --- a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Metadata; -[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Struct)] public sealed class PrivateApiAttribute : Attribute { diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index 99a9a9d7fa..82f54a7b71 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,4 +1,5 @@ using Avalonia.Input; +using Avalonia.Media.Imaging; using Avalonia.Metadata; #nullable enable @@ -9,6 +10,6 @@ namespace Avalonia.Platform public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); - ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); + ICursorImpl CreateCursor(Bitmap cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 848620dae2..54310eefa6 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -228,7 +228,7 @@ namespace Avalonia.Platform (T?)context.GetFeature(typeof(T)); } - public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl + public interface IDrawingContextLayerImpl : IBitmapImpl { /// /// Does optimized blit with Src blend mode. @@ -240,6 +240,16 @@ namespace Avalonia.Platform /// Returns true if layer supports optimized blit. /// bool CanBlit { get; } + + /// + /// Indicates if the render target is no longer usable and needs to be recreated + /// + bool IsCorrupted { get; } + + /// + /// Creates drawing context. It matches the properties of the original drawing context this layer was created from. + /// + IDrawingContextImpl CreateDrawingContext(); } public interface IDrawingContextLayerWithRenderContextAffinityImpl : IDrawingContextLayerImpl diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 3a42a88aed..bffc00235b 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; +using Avalonia.Platform.Surfaces; namespace Avalonia.Platform { @@ -214,7 +215,7 @@ namespace Avalonia.Platform /// The list of native platform surfaces that can be used for output. /// /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); + IRenderTarget CreateRenderTarget(IEnumerable surfaces); /// /// Creates an offscreen render target @@ -239,5 +240,10 @@ namespace Avalonia.Platform /// Maximum supported offscreen render target pixel size, or null if no limit /// public PixelSize? MaxOffscreenRenderTargetPixelSize { get; } + + /// + /// Checks if a render target can be created for the given surfaces and the preferred surface is ready + /// + bool IsReadyToCreateRenderTarget(IEnumerable surfaces) => true; } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index a31e7e550a..e66d14995e 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -25,16 +25,18 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// Apply DPI reported by the render target as a hidden transform matrix - IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing); + /// Information about the scene that's about to be rendered into this render target. + /// This is expected to be reported to the underlying platform and affect the framebuffer size, however + /// the implementation may choose to ignore that information. + /// + /// Returns various properties about the returned drawing context + IDrawingContextImpl CreateDrawingContext(RenderTargetSceneInfo sceneInfo, out RenderTargetDrawingContextProperties properties); /// - /// Creates an for a rendering session. + /// Indicates if the render target is currently ready to be rendered to /// - /// The pixel size of the surface - /// Returns various properties about the returned drawing context - IDrawingContextImpl CreateDrawingContext( - PixelSize expectedPixelSize, - out RenderTargetDrawingContextProperties properties); + bool IsReady => true; + + public record struct RenderTargetSceneInfo(PixelSize Size, double Scaling); } } diff --git a/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs b/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs index d33c503650..aab734c7c9 100644 --- a/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs @@ -7,7 +7,8 @@ namespace Avalonia.Platform /// . /// [Unstable] - public interface IRenderTargetBitmapImpl : IBitmapImpl, IRenderTarget + public interface IRenderTargetBitmapImpl : IReadableBitmapImpl { + IDrawingContextImpl CreateDrawingContext(); } } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs similarity index 100% rename from src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs rename to src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs diff --git a/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs new file mode 100644 index 0000000000..794e2cc229 --- /dev/null +++ b/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Surfaces +{ + [Unstable] + public interface IFramebufferPlatformSurface : IPlatformRenderSurface + { + IFramebufferRenderTarget CreateFramebufferRenderTarget(); + } + + + [PrivateApi] + public interface IFramebufferRenderTarget : IDisposable, IPlatformRenderSurfaceRenderTarget + { + /// + /// Provides a framebuffer descriptor for drawing. + /// + /// + /// Contents should be drawn on actual window after disposing + /// + ILockedFramebuffer Lock(IRenderTarget.RenderTargetSceneInfo sceneInfo, out FramebufferLockProperties properties); + + bool RetainsFrameContents => false; + } + + [PrivateApi] + public record struct FramebufferLockProperties(bool PreviousFrameIsRetained); + + /// + /// For simple cases when framebuffer is always available + /// + public class FuncFramebufferRenderTarget : IFramebufferRenderTarget + { + public delegate ILockedFramebuffer LockFramebufferDelegate(IRenderTarget.RenderTargetSceneInfo sceneInfo, out FramebufferLockProperties properties); + private readonly LockFramebufferDelegate _lockFramebuffer; + + public FuncFramebufferRenderTarget(Func lockFramebuffer) : + this((_, out properties) => + { + properties = default; + return lockFramebuffer(); + }) + { + + } + + + public FuncFramebufferRenderTarget(LockFramebufferDelegate lockFramebuffer, bool retainsFrameContents = false) + { + _lockFramebuffer = lockFramebuffer; + RetainsFrameContents = retainsFrameContents; + } + + public void Dispose() + { + // No-op + } + + public ILockedFramebuffer Lock(IRenderTarget.RenderTargetSceneInfo sceneInfo, + out FramebufferLockProperties properties) => _lockFramebuffer(sceneInfo, out properties); + + public bool RetainsFrameContents { get; } + } + +} diff --git a/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs b/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs new file mode 100644 index 0000000000..ff71a700c4 --- /dev/null +++ b/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs @@ -0,0 +1,15 @@ +using Avalonia.Metadata; + +namespace Avalonia.Platform.Surfaces; + +[PrivateApi] +public interface IPlatformRenderSurface +{ + bool IsReady => true; +} + +[PrivateApi] +public interface IPlatformRenderSurfaceRenderTarget +{ + bool IsReady => true; +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 66ee5579c8..1bff835f17 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Diagnostics; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Threading; @@ -48,7 +50,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester /// /// A function returning the list of native platform's surfaces that can be consumed by rendering subsystems. /// - public CompositingRenderer(IPresentationSource root, Compositor compositor, Func> surfaces) + public CompositingRenderer(IPresentationSource root, Compositor compositor, Func> surfaces) { _root = root; _compositor = compositor; diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 8a75681c68..b1ff53e5c8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -12,9 +14,9 @@ public partial class Compositor /// /// A factory method to create IRenderTarget to be called from the render thread /// - internal CompositionTarget CreateCompositionTarget(Func> surfaces) + internal CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index e8cd29f195..2398468456 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -37,29 +37,12 @@ namespace Avalonia.Rendering.Composition private readonly object _pendingBatchLock = new(); private readonly List _pendingServerCompositorJobs = new(); private readonly List _pendingServerCompositorPostTargetJobs = new(); - private DiagnosticTextRenderer? _diagnosticTextRenderer; private readonly Action _triggerCommitRequested; internal IEasing DefaultEasing { get; } internal Dispatcher Dispatcher { get; } - private DiagnosticTextRenderer? DiagnosticTextRenderer - { - get - { - if (_diagnosticTextRenderer == null) - { - // We are running in some unit test context - if (AvaloniaLocator.Current.GetService() == null) - return null; - _diagnosticTextRenderer = new(Typeface.Default.GlyphTypeface, 12.0); - } - - return _diagnosticTextRenderer; - } - } - internal event Action? AfterCommit; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs index a8c2908b88..9873fe6ff3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs @@ -17,20 +17,16 @@ internal class CompositionTargetOverlays private Rect? _oldFpsCounterRect; private long _updateStarted; private readonly ServerCompositionTarget _target; - private readonly DiagnosticTextRenderer? _diagnosticTextRenderer; - public CompositionTargetOverlays( - ServerCompositionTarget target, - DiagnosticTextRenderer? diagnosticTextRenderer) + public CompositionTargetOverlays(ServerCompositionTarget target) { _target = target; - _diagnosticTextRenderer = diagnosticTextRenderer; } private RendererDebugOverlays DebugOverlays { get; set; } private FpsCounter? FpsCounter - => _fpsCounter ??= _diagnosticTextRenderer != null ? new FpsCounter(_diagnosticTextRenderer) : null; + => _fpsCounter ??= DiagnosticTextRenderer is { } diagnosticTextRenderer ? new FpsCounter(diagnosticTextRenderer) : null; private FrameTimeGraph? LayoutTimeGraph => _layoutTimeGraph ??= CreateTimeGraph("Layout"); @@ -44,15 +40,29 @@ internal class CompositionTargetOverlays private FrameTimeGraph? UpdateTimeGraph => _updateTimeGraph ??= CreateTimeGraph("TUpdate"); + private DiagnosticTextRenderer? DiagnosticTextRenderer + { + get + { + if (field is null) + { + // We are running in some unit test context + if (AvaloniaLocator.Current.GetService() == null) + return null; + field = new DiagnosticTextRenderer(Typeface.Default.GlyphTypeface, 12.0); + } + return field; + } + } public bool RequireLayer => DebugOverlays.HasAnyFlag(RendererDebugOverlays.DirtyRects); private FrameTimeGraph? CreateTimeGraph(string title) { - if (_diagnosticTextRenderer == null) + if (DiagnosticTextRenderer is not { } diagnosticTextRenderer) return null; - return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); + return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, diagnosticTextRenderer); } @@ -73,6 +83,8 @@ internal class CompositionTargetOverlays if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) { _renderTimeGraph?.Reset(); + _compositorUpdateTimeGraph?.Reset(); + _updateTimeGraph?.Reset(); } } @@ -170,4 +182,4 @@ internal class CompositionTargetOverlays LayoutTimeGraph?.AddFrameValue(lastLayoutPassTiming.Elapsed.TotalMilliseconds); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index b7a1993510..f8382547b9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -8,6 +8,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Transport; using Avalonia.Utilities; @@ -20,7 +21,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; - private readonly Func> _surfaces; + private readonly Func> _surfaces; private CompositionTargetOverlays _overlays; private static long s_nextId = 1; private IRenderTarget? _renderTarget; @@ -39,13 +40,12 @@ namespace Avalonia.Rendering.Composition.Server public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, - DiagnosticTextRenderer? diagnosticTextRenderer) + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; _surfaces = surfaces; - _overlays = new CompositionTargetOverlays(this, diagnosticTextRenderer); + _overlays = new CompositionTargetOverlays(this); var platformRender = AvaloniaLocator.Current.GetService(); if (platformRender?.SupportsRegions == true && compositor.Options.UseRegionDirtyRectClipping != false) @@ -142,6 +142,8 @@ namespace Avalonia.Rendering.Composition.Server try { + if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces())) + return; _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); } catch (RenderTargetNotReadyException) @@ -161,13 +163,15 @@ namespace Avalonia.Rendering.Composition.Server if (!_redrawRequested) return; + if (!_renderTarget.IsReady) + return; + var needLayer = _overlays.RequireLayer // Check if we don't need overlays // Check if render target can be rendered to directly and preserves the previous frame || !(_renderTarget.Properties.RetainsPreviousFrameContents && _renderTarget.Properties.IsSuitableForDirectRendering); - using (var renderTargetContext = _renderTarget.CreateDrawingContext( - this.PixelSize, out var properties)) + using (var renderTargetContext = _renderTarget.CreateDrawingContext(new(PixelSize, Scaling), out var properties)) using (var renderTiming = Diagnostic.BeginCompositorRenderPass()) { var fullRedraw = false; @@ -204,7 +208,7 @@ namespace Avalonia.Rendering.Composition.Server DirtyRects.FinalizeFrame(renderBounds); if (_layer != null) { - using (var context = _layer.CreateDrawingContext(false)) + using (var context = _layer.CreateDrawingContext()) RenderRootToContextWithClip(context, Root); renderTargetContext.Clear(Colors.Transparent); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs index a9a481b5e3..973ac8a834 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs @@ -193,7 +193,7 @@ internal class ServerCompositionVisualCache // Render to layer if needed if (!_dirtyRectTracker.IsEmpty) { - using var ctx = _layer.CreateDrawingContext(false); + using var ctx = _layer.CreateDrawingContext(); using (_needsFullReRender ? null : _dirtyRectTracker.BeginDraw(ctx)) { ctx.Clear(Colors.Transparent); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index d299bed384..e440ffab26 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -62,7 +62,7 @@ internal partial class ServerCompositor try { target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, new(scaling, scaling), true); - using (var canvas = target.CreateDrawingContext(false)) + using (var canvas = target.CreateDrawingContext()) { canvas.Transform = scaleTransform; visual.Render(canvas, LtrbRect.Infinite, null, renderChildren); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e23197ff13..76e649407f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -274,12 +275,17 @@ namespace Avalonia.Rendering.Composition.Server _activeTargets.Remove(target); } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { using (RenderInterface.EnsureCurrent()) return RenderInterface.CreateRenderTarget(surfaces); } + public bool IsReadyToCreateRenderTarget(IEnumerable surfaces) + { + return RenderInterface.IsReadyToCreateRenderTarget(surfaces); + } + public bool CheckAccess() => _safeThread == Thread.CurrentThread; public void VerifyAccess() { diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 6aabb4d168..7e1c9e711f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Avalonia.Platform; using Avalonia.Threading; @@ -30,9 +31,7 @@ internal abstract class BatchStreamPoolBase : IDisposable var updateRef = new WeakReference>(this); if ( reclaimImmediately - || ( - AvaloniaLocator.Current.GetService() == null - && AvaloniaLocator.Current.GetService() == null)) + || Dispatcher.FromThread(Thread.CurrentThread) == null) _reclaimImmediately = true; else StartUpdateTimer(startTimer, updateRef); diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index db61ad84f1..980c7818c7 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Reactive; namespace Avalonia.Rendering; @@ -77,9 +78,16 @@ internal class PlatformRenderInterfaceContextManager return Disposable.Empty; } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { EnsureValidBackendContext(); return _backend!.CreateRenderTarget(surfaces); } + + public bool IsReadyToCreateRenderTarget(IEnumerable surfaces) + { + if (_backend == null) + return IsReady; + return _backend.IsReadyToCreateRenderTarget(surfaces); + } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d1b960390c..f511d1de2a 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -76,7 +76,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); + [ThreadStatic] private static ControlTheme? s_invalidTheme; private int _initCount; private string? _name; private Classes? _classes; @@ -332,6 +332,9 @@ namespace Avalonia /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + internal static ControlTheme InvalidTheme + => s_invalidTheme ??= new(); + /// public virtual void BeginInit() { @@ -666,10 +669,10 @@ namespace Avalonia if (this.TryFindResource(key, out var value) && value is ControlTheme t) _implicitTheme = t; else - _implicitTheme = s_invalidTheme; + _implicitTheme = InvalidTheme; } - if (_implicitTheme != s_invalidTheme) + if (_implicitTheme != InvalidTheme) return _implicitTheme; return null; @@ -828,11 +831,11 @@ namespace Avalonia return; // Refetch the implicit theme. - var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var oldImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; _implicitTheme = null; GetEffectiveTheme(); - var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var newImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; // If the implicit theme has changed, detach the existing theme. if (newImplicitTheme != oldImplicitTheme) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 954183ffcc..09dd9f27ec 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -12,7 +12,7 @@ public partial class Dispatcher private bool _explicitBackgroundProcessingRequested; private const int MaximumInputStarvationTimeInFallbackMode = 50; private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; - private readonly int _maximumInputStarvationTime; + private int _maximumInputStarvationTime; void RequestBackgroundProcessing() { @@ -101,9 +101,9 @@ public partial class Dispatcher internal static void ResetBeforeUnitTests() { - s_uiThread = null; + ResetGlobalState(); } - + internal static void ResetForUnitTests() { if (s_uiThread == null) @@ -122,14 +122,14 @@ public partial class Dispatcher if (job == null || job.Priority <= DispatcherPriority.Inactive) { s_uiThread.ShutdownImpl(); - s_uiThread = null; + ResetGlobalState(); return; } s_uiThread.ExecuteJob(job); } - } + private void ExecuteJob(DispatcherOperation job) { diff --git a/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs new file mode 100644 index 0000000000..7d9d0b39cf --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + [ThreadStatic] + private static DispatcherReferenceStorage? s_currentThreadDispatcher; + private static readonly object s_globalLock = new(); + private static readonly ConditionalWeakTable s_dispatchers = new(); + + private static Dispatcher? s_uiThread; + + // This class is needed PURELY for ResetForUnitTests, so we can reset s_currentThreadDispatcher for all threads + class DispatcherReferenceStorage + { + public WeakReference Reference = new(null!); + } + + public static Dispatcher CurrentDispatcher + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (s_currentThreadDispatcher?.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + + return new Dispatcher(null); + } + } + + public static Dispatcher? FromThread(Thread thread) + { + lock (s_globalLock) + { + if (s_dispatchers.TryGetValue(thread, out var reference) && reference.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + return null; + } + } + + public static Dispatcher UIThread + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + static Dispatcher GetUIThreadDispatcherSlow() + { + lock (s_globalLock) + { + return s_uiThread ?? CurrentDispatcher; + } + } + return s_uiThread ?? GetUIThreadDispatcherSlow(); + } + } + + internal static Dispatcher? TryGetUIThread() + { + lock (s_globalLock) + return s_uiThread; + } + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IPlatformThreadingInterface impl) => + InitializeUIThreadDispatcher(new LegacyDispatcherImpl(impl)); + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IDispatcherImpl impl) + { + UIThread.VerifyAccess(); + if (UIThread._initialized) + throw new InvalidOperationException("UI thread dispatcher is already initialized"); + UIThread.ReplaceImplementation(impl); + } + + private static void ResetGlobalState() + { + lock (s_globalLock) + { + foreach (var store in s_dispatchers) + store.Value.Reference = new(null!); + s_dispatchers.Clear(); + + s_currentThreadDispatcher = null; + s_uiThread = null; + } + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index ce16820286..9966a156d2 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Avalonia.Threading; @@ -15,7 +16,8 @@ public partial class Dispatcher private long? _dueTimeForBackgroundProcessing; private long? _osTimerSetTo; - internal long Now => _impl.Now; + private readonly Func _timeProvider; + internal long Now => _timeProvider(); private void UpdateOSTimer() { @@ -26,6 +28,7 @@ public partial class Dispatcher _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; if (_osTimerSetTo == nextDueTime) return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); } @@ -114,7 +117,8 @@ public partial class Dispatcher bool needToProcessQueue = false; lock (InstanceLock) { - _impl.UpdateTimer(_osTimerSetTo = null); + _impl.UpdateTimer(null); + _osTimerSetTo = null; needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; if (needToPromoteTimers) _dueTimeForTimers = null; diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 8253c2fed2..07582ac3f4 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -3,7 +3,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Threading; @@ -17,63 +20,60 @@ namespace Avalonia.Threading; public partial class Dispatcher : IDispatcher { private IDispatcherImpl _impl; + private bool _initialized; internal object InstanceLock { get; } = new(); private IControlledDispatcherImpl? _controlledImpl; - private static Dispatcher? s_uiThread; private IDispatcherImplWithPendingInput? _pendingInputImpl; - private readonly IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private readonly Thread _thread; private readonly AvaloniaSynchronizationContext?[] _priorityContexts = new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1]; - internal Dispatcher(IDispatcherImpl impl) + internal Dispatcher(IDispatcherImpl? impl) { - _impl = impl; - impl.Timer += OnOSTimer; - impl.Signaled += Signaled; - _controlledImpl = _impl as IControlledDispatcherImpl; - _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; - _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; - _maximumInputStarvationTime = _backgroundProcessingImpl == null ? - MaximumInputStarvationTimeInFallbackMode : - MaximumInputStarvationTimeInExplicitProcessingExplicitMode; - if (_backgroundProcessingImpl != null) - _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; - - _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); - _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); - } - - public static Dispatcher UIThread - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get +#if DEBUG + if (AvaloniaLocator.Current.GetService() != null + || AvaloniaLocator.Current.GetService() != null) + throw new InvalidOperationException( + "Registering IDispatcherImpl or IPlatformThreadingInterface via locator is no longer valid"); +#endif + lock (s_globalLock) { - return s_uiThread ??= CreateUIThreadDispatcher(); - } - } + _thread = Thread.CurrentThread; + if (FromThread(_thread) != null) + throw new InvalidOperationException("The current thread already has a dispatcher"); - public bool SupportsRunLoops => _controlledImpl != null; + // The first created dispatcher becomes "UI thread one" + s_uiThread ??= this; - [MethodImpl(MethodImplOptions.NoInlining)] - private static Dispatcher CreateUIThreadDispatcher() - { - var impl = AvaloniaLocator.Current.GetService(); - if (impl == null) + s_dispatchers.Remove(Thread.CurrentThread); + s_dispatchers.Add(Thread.CurrentThread, + s_currentThreadDispatcher = new() { Reference = new WeakReference(this) }); + } + + if (impl is null) { - var platformThreading = AvaloniaLocator.Current.GetService(); - if (platformThreading != null) - impl = new LegacyDispatcherImpl(platformThreading); - else - impl = new NullDispatcherImpl(); + var st = Stopwatch.StartNew(); + _timeProvider = () => st.ElapsedMilliseconds; } - return new Dispatcher(impl); + else + _timeProvider = () => impl.Now; + + _impl = null!; // Set by ReplaceImplementation + ReplaceImplementation(impl); + + + _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); + _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); } + public bool SupportsRunLoops => _controlledImpl != null; + /// /// Checks that the current thread is the UI thread. /// - public bool CheckAccess() => _impl.CurrentThreadIsLoopThread; + public bool CheckAccess() => Thread.CurrentThread == _thread; /// /// Checks that the current thread is the UI thread and throws if not. @@ -89,15 +89,64 @@ public partial class Dispatcher : IDispatcher [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] static void ThrowVerifyAccess() - => throw new InvalidOperationException("Call from invalid thread"); + => throw new InvalidOperationException("The calling thread cannot access this object because a different thread owns it."); ThrowVerifyAccess(); } } + public Thread Thread => _thread; + internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority) { DispatcherPriority.Validate(priority, nameof(priority)); var index = priority - DispatcherPriority.MinValue; return _priorityContexts[index] ??= new(this, priority); } + + [PrivateApi] + public IDispatcherImpl PlatformImpl => _impl; + + private void ReplaceImplementation(IDispatcherImpl? impl) + { + // TODO: Consider moving the helper out of Avalonia.Win32 so + // it's usable earlier + using var _ = NonPumpingLockHelper.Use(); + + + if (impl?.CurrentThreadIsLoopThread == false) + throw new InvalidOperationException("IDispatcherImpl belongs to a different thread"); + + if (_impl != null!) // Null in ctor + { + _impl.Timer -= OnOSTimer; + _impl.Signaled -= Signaled; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing -= OnReadyForExplicitBackgroundProcessing; + _impl = null!; + _controlledImpl = null; + _pendingInputImpl = null; + _backgroundProcessingImpl = null; + } + + if (impl != null) + _initialized = true; + else + impl = new ManagedDispatcherImpl(null); + _impl = impl; + + impl.Timer += OnOSTimer; + impl.Signaled += Signaled; + _controlledImpl = _impl as IControlledDispatcherImpl; + _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; + _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + _maximumInputStarvationTime = _backgroundProcessingImpl == null ? + MaximumInputStarvationTimeInFallbackMode : + MaximumInputStarvationTimeInExplicitProcessingExplicitMode; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; + if (_signaled) + _impl.Signal(); + _osTimerSetTo = null; + UpdateOSTimer(); + } } diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index dd438b176e..f8d5cb8947 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -80,33 +80,4 @@ internal class LegacyDispatcherImpl : IDispatcherImpl _timer = null; Timer?.Invoke(); } -} - -internal sealed class NullDispatcherImpl : IDispatcherImpl -{ - public bool CurrentThreadIsLoopThread => true; - - public void Signal() - { - - } - - public event Action? Signaled - { - add { } - remove { } - } - - public event Action? Timer - { - add { } - remove { } - } - - public long Now => 0; - - public void UpdateTimer(long? dueTimeInMs) - { - - } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 1ebf4f6457..2c18836d75 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,19 +1,12 @@ using System; using System.Runtime.CompilerServices; -#if !BUILDTASK -using Avalonia.Metadata; -#endif namespace Avalonia.Utilities { /// /// Provides math utilities not provided in System.Math. /// -#if !BUILDTASK - [Unstable("This API might be removed in next major version. Please use corresponding BCL APIs.")] - public -#endif - static class MathUtilities + internal static class MathUtilities { // smallest such that 1.0+DoubleEpsilon != 1.0 internal const double DoubleEpsilon = 2.2204460492503131e-016; diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimation.cs b/src/Avalonia.Controls/Animation/ConnectedAnimation.cs new file mode 100644 index 0000000000..083e0fd604 --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimation.cs @@ -0,0 +1,797 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Styling; +using Avalonia.Logging; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Animation +{ + /// + /// Provides data for the event. + /// + internal sealed class ConnectedAnimationCompletedEventArgs : EventArgs + { + internal ConnectedAnimationCompletedEventArgs(bool cancelled) => Cancelled = cancelled; + + /// + /// Gets a value indicating whether the animation was cancelled before it completed. + /// When the destination element's opacity has already been + /// restored but no visual transition was shown. + /// + public bool Cancelled { get; } + } + + /// + /// Animates an element seamlessly between two views during navigation by flying a + /// proxy over the . + /// + /// + /// + /// Obtain an instance via , + /// then start it with after navigation. + /// + /// + /// The animation auto-disposes after three seconds if not consumed (matching UWP behaviour). + /// + /// + internal class ConnectedAnimation : IDisposable + { + private const double CoordinatedFadeStartThreshold = 0.6; + private const double CoordinatedFadeRange = 0.4; + + private readonly string _key; + private readonly ConnectedAnimationService _service; + + private Rect _sourceBounds; + private CornerRadius _sourceCornerRadius; + private IBrush? _sourceBackground; + private Thickness _sourceBorderThickness; + private IBrush? _sourceBorderBrush; + private RenderTargetBitmap? _sourceSnapshot; + + private bool _isConsumed; + private bool _disposed; + + private CancellationTokenSource? _timeoutCts; + private IDisposable? _timeoutTimerDisposable; + private CancellationTokenSource? _animationCts; + private DispatcherTimer? _animationTimer; + + private static readonly SplineEasing s_directEasing = new(0, 0, 0.58, 1.0); + private static readonly SplineEasing s_basicEasing = new(0.42, 0, 0.58, 1); + private static readonly SplineEasing s_gravityEasing = new(0.1, 0.9, 0.2, 1.0); + + // Active-flight state used by Dispose to clean up if cancelled mid-animation. + private Visual? _activeDestination; + private double _activeDestOriginalOpacity; + private Border? _activeProxy; + private OverlayLayer? _activeOverlayLayer; + + internal ConnectedAnimation(string key, Visual source, ConnectedAnimationService service) + { + _key = key; + _service = service; + + _sourceCornerRadius = GetCornerRadius(source); + _sourceBackground = GetBackground(source); + _sourceBorderThickness = GetBorderThickness(source); + _sourceBorderBrush = GetBorderBrush(source); + + var topLevel = source.FindAncestorOfType(); + if (topLevel != null && source.Bounds.Width > 0 && source.Bounds.Height > 0) + { + var transform = source.TransformToVisual(topLevel); + if (transform.HasValue) + { + _sourceBounds = new Rect( + transform.Value.Transform(new Point(0, 0)), + new Size(source.Bounds.Width, source.Bounds.Height)); + } + + CaptureSnapshot(source, topLevel); + } + + // Auto-dispose after 3 s if not consumed (matches UWP behaviour). + _timeoutCts = new CancellationTokenSource(); + var token = _timeoutCts.Token; + _timeoutTimerDisposable = DispatcherTimer.RunOnce(() => + { + if (!token.IsCancellationRequested && !_isConsumed) + Dispose(); + }, TimeSpan.FromSeconds(3), DispatcherPriority.Background); + } + + /// Gets the key that identifies this animation. + public string Key => _key; + + /// Gets a value indicating whether TryStart has been called. + public bool IsConsumed => _isConsumed; + + /// + /// Gets or sets the configuration that controls timing and visual style. + /// Set this before calling TryStart. + /// + public ConnectedAnimationConfiguration? Configuration { get; set; } + + /// + /// Raised when the animation finishes or is cancelled. + /// Check to distinguish the cases. + /// + public event EventHandler? Completed; + + /// + /// Starts the animation towards . + /// Returns if the animation has already been consumed or disposed. + /// + public bool TryStart(Visual destination) + { + ArgumentNullException.ThrowIfNull(destination); + return TryStart(destination, Array.Empty()); + } + + /// + /// Starts the animation towards with optional + /// that fade in during the last 40 % of the animation. + /// Returns if the animation has already been consumed or disposed. + /// + public bool TryStart(Visual destination, IReadOnlyList coordinatedElements) + { + ArgumentNullException.ThrowIfNull(destination); + ArgumentNullException.ThrowIfNull(coordinatedElements); + if (_isConsumed || _disposed) + return false; + + _isConsumed = true; + CancelTimeout(); + + _ = RunAnimationAsync(destination, coordinatedElements); + return true; + } + + // Exposed internally so tests can verify disposal state without reflection. + internal bool IsDisposed => _disposed; + + /// + /// Releases all resources and cancels the animation if it is in flight. + /// The event is raised with Cancelled = true + /// only when the animation was actively running at dispose time. + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + CancelTimeout(); + _service.RemoveAnimation(_key); + _animationCts?.Cancel(); + _animationCts?.Dispose(); + _animationCts = null; + _animationTimer?.Stop(); + _animationTimer = null; + + var wasMidFlight = _activeDestination != null; + + if (_activeDestination != null) + { + _activeDestination.Opacity = _activeDestOriginalOpacity; + _activeDestination = null; + } + + if (_activeProxy != null && _activeOverlayLayer != null) + { + _activeOverlayLayer.Children.Remove(_activeProxy); + _activeProxy = null; + _activeOverlayLayer = null; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + if (wasMidFlight) + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: true)); + } + + private void CaptureSnapshot(Visual source, TopLevel topLevel) + { + try + { + var dpi = topLevel.RenderScaling; + var w = (int)Math.Ceiling(source.Bounds.Width * dpi); + var h = (int)Math.Ceiling(source.Bounds.Height * dpi); + if (w > 0 && h > 0) + { + _sourceSnapshot = new RenderTargetBitmap( + new PixelSize(w, h), + new Vector(96 * dpi, 96 * dpi)); + _sourceSnapshot.Render(source); + } + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual) + ?.Log(this, "ConnectedAnimation snapshot failed for key '{Key}': {Exception}", Key, ex); + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + } + } + + private void CancelTimeout() + { + _timeoutTimerDisposable?.Dispose(); + _timeoutTimerDisposable = null; + _timeoutCts?.Cancel(); + _timeoutCts?.Dispose(); + _timeoutCts = null; + } + + private async Task RunAnimationAsync(Visual destination, IReadOnlyList coordinatedElements) + { + try + { + await RunAnimationCoreAsync(destination, coordinatedElements); + } + catch (OperationCanceledException) + { + // Dispose already handles cleanup. + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual) + ?.Log(this, "ConnectedAnimation failed for key '{Key}': {Exception}", Key, ex); + Dispose(); + } + } + + private async Task RunAnimationCoreAsync(Visual destination, IReadOnlyList coordinatedElements) + { + ResolveTimingAndEasing(_service, out var duration, out var easing, + out var useGravityDip, out var useShadow); + + var topLevel = destination.FindAncestorOfType(); + if (topLevel == null) + { + OnAnimationComplete(); + return; + } + + var overlayLayer = OverlayLayer.GetOverlayLayer(topLevel); + if (overlayLayer == null) + { + await RunFallbackAnimationAsync(destination, coordinatedElements, + topLevel, duration, easing, useGravityDip, useShadow); + return; + } + + // Wait for destination layout if bounds are not yet valid. + if (destination.Bounds.Width <= 0 || destination.Bounds.Height <= 0 || + !destination.TransformToVisual(topLevel).HasValue) + { + if (destination is Layoutable layoutable) + { + var layoutTcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + EventHandler? handler = null; + handler = (_, _) => + { + if (destination.Bounds.Width > 0 && destination.Bounds.Height > 0 && + destination.TransformToVisual(topLevel).HasValue) + { + layoutable.LayoutUpdated -= handler; + layoutTcs.TrySetResult(true); + } + }; + layoutable.LayoutUpdated += handler; + + using var reg = timeoutCts.Token.Register(() => + { + layoutable.LayoutUpdated -= handler; + layoutTcs.TrySetResult(false); + }); + + await layoutTcs.Task; + } + } + + var destTransform = destination.TransformToVisual(topLevel); + if (!destTransform.HasValue) + { + OnAnimationComplete(); + return; + } + + var destBounds = new Rect( + destTransform.Value.Transform(new Point(0, 0)), + new Size(destination.Bounds.Width, destination.Bounds.Height)); + + var destCornerRadius = GetCornerRadius(destination); + var destBorderThickness = GetBorderThickness(destination); + var destBorderBrush = GetBorderBrush(destination); + + var proxy = new ConnectedAnimationProxy + { + Width = _sourceBounds.Width, + Height = _sourceBounds.Height, + CornerRadius = _sourceCornerRadius, + BorderThickness = _sourceBorderThickness, + BorderBrush = _sourceBorderBrush, + ClipToBounds = true, + IsHitTestVisible = false, + }; + + if (_sourceBackground != null) + proxy.Background = _sourceBackground; + else if (_sourceSnapshot != null) + proxy.Background = new ImageBrush(_sourceSnapshot) { Stretch = Stretch.Fill }; + + Canvas.SetLeft(proxy, _sourceBounds.X); + Canvas.SetTop(proxy, _sourceBounds.Y); + + var destOriginalOpacity = destination.Opacity; + destination.Opacity = 0; + + _activeDestination = destination; + _activeDestOriginalOpacity = destOriginalOpacity; + _activeProxy = proxy; + _activeOverlayLayer = overlayLayer; + + var originalOpacities = new double[coordinatedElements.Count]; + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + originalOpacities[i] = coordinatedElements[i].Opacity; + coordinatedElements[i].Opacity = 0; + } + + var destBackground = GetBackground(destination); + var needsCrossFade = destBackground != null + && _sourceBackground != null + && !BrushesEqual(_sourceBackground, destBackground); + + overlayLayer.Children.Add(proxy); + + Border? crossFadeOverlay = null; + if (needsCrossFade) + { + crossFadeOverlay = new Border + { + Background = destBackground, + Opacity = 0, + IsHitTestVisible = false, + }; + proxy.Child = crossFadeOverlay; + } + + var startX = _sourceBounds.X; var endX = destBounds.X; + var startY = _sourceBounds.Y; var endY = destBounds.Y; + var startW = _sourceBounds.Width; var endW = destBounds.Width; + var startH = _sourceBounds.Height; var endH = destBounds.Height; + + var srcTL = _sourceCornerRadius.TopLeft; + var srcTR = _sourceCornerRadius.TopRight; + var srcBR = _sourceCornerRadius.BottomRight; + var srcBL = _sourceCornerRadius.BottomLeft; + var dstTL = destCornerRadius.TopLeft; + var dstTR = destCornerRadius.TopRight; + var dstBR = destCornerRadius.BottomRight; + var dstBL = destCornerRadius.BottomLeft; + + var srcBT = _sourceBorderThickness; + var dstBT = destBorderThickness; + + var canLerpBorderBrush = _sourceBorderBrush is ISolidColorBrush && destBorderBrush is ISolidColorBrush; + var srcBC = (_sourceBorderBrush as ISolidColorBrush)?.Color ?? default; + var dstBC = (destBorderBrush as ISolidColorBrush)?.Color ?? default; + SolidColorBrush? lerpBrush = canLerpBorderBrush ? new SolidColorBrush(srcBC) : null; + var snapBorderBrush = !canLerpBorderBrush && destBorderBrush != null; + + double dipAmplitude = 0, scaleAmplitude = 0; + if (useGravityDip) + { + var travel = Math.Max(Math.Abs(endX - startX), Math.Abs(endY - startY)); + dipAmplitude = Math.Clamp(travel * 0.12, 8, 50); + scaleAmplitude = 0.05; + } + + var animateShadow = useShadow && useGravityDip; + + _animationCts = new CancellationTokenSource(); + + proxy.ProgressCallback = progress => + { + var ep = easing.Ease(progress); + + var bx = startX + (endX - startX) * ep; + var by = startY + (endY - startY) * ep; + var bw = startW + (endW - startW) * ep; + var bh = startH + (endH - startH) * ep; + + if (useGravityDip) + { + var dipCurve = Math.Sin(Math.PI * progress); + var scaleBoost = 1.0 + scaleAmplitude * dipCurve; + var sw = bw * scaleBoost; + var sh = bh * scaleBoost; + + Canvas.SetLeft(proxy, bx - (sw - bw) / 2); + Canvas.SetTop(proxy, by - (sh - bh) / 2 + dipAmplitude * dipCurve); + proxy.Width = Math.Max(1, sw); + proxy.Height = Math.Max(1, sh); + + if (animateShadow) + { + var alpha = (byte)(100 * dipCurve); + var blur = 24 * dipCurve; + var offsetY = 10 * dipCurve; + proxy.BoxShadow = new BoxShadows(new BoxShadow + { + OffsetX = 0, OffsetY = offsetY, + Blur = blur, + Color = Color.FromArgb(alpha, 0, 0, 0) + }); + } + } + else + { + Canvas.SetLeft(proxy, bx); + Canvas.SetTop(proxy, by); + proxy.Width = Math.Max(1, bw); + proxy.Height = Math.Max(1, bh); + } + + proxy.CornerRadius = new CornerRadius( + srcTL + (dstTL - srcTL) * ep, + srcTR + (dstTR - srcTR) * ep, + srcBR + (dstBR - srcBR) * ep, + srcBL + (dstBL - srcBL) * ep); + + proxy.BorderThickness = new Thickness( + srcBT.Left + (dstBT.Left - srcBT.Left) * ep, + srcBT.Top + (dstBT.Top - srcBT.Top) * ep, + srcBT.Right + (dstBT.Right - srcBT.Right) * ep, + srcBT.Bottom + (dstBT.Bottom - srcBT.Bottom) * ep); + + if (lerpBrush != null) + { + lerpBrush.Color = Color.FromArgb( + (byte)(srcBC.A + (dstBC.A - srcBC.A) * ep), + (byte)(srcBC.R + (dstBC.R - srcBC.R) * ep), + (byte)(srcBC.G + (dstBC.G - srcBC.G) * ep), + (byte)(srcBC.B + (dstBC.B - srcBC.B) * ep)); + proxy.BorderBrush = lerpBrush; + } + else if (snapBorderBrush && progress >= 0.5) + { + proxy.BorderBrush = destBorderBrush; + snapBorderBrush = false; + } + + if (crossFadeOverlay != null) + crossFadeOverlay.Opacity = ep; + + if (progress > CoordinatedFadeStartThreshold) + { + var cp = (progress - CoordinatedFadeStartThreshold) / CoordinatedFadeRange; + for (int j = 0; j < coordinatedElements.Count; j++) + { + if (ReferenceEquals(coordinatedElements[j], destination)) + continue; + coordinatedElements[j].Opacity = originalOpacities[j] * cp; + } + } + }; + + var animation = new Avalonia.Animation.Animation + { + Duration = duration, + Easing = new LinearEasing(), + FillMode = FillMode.Forward, + Children = + { + new KeyFrame { Cue = new Cue(0), Setters = { new Setter(ConnectedAnimationProxy.ProgressProperty, 0.0) } }, + new KeyFrame { Cue = new Cue(1), Setters = { new Setter(ConnectedAnimationProxy.ProgressProperty, 1.0) } }, + } + }; + + await animation.RunAsync(proxy, _animationCts.Token); + + _animationCts?.Dispose(); + _animationCts = null; + + destination.Opacity = destOriginalOpacity; + + _activeDestination = null; + _activeProxy = null; + _activeOverlayLayer = null; + + overlayLayer.Children.Remove(proxy); + + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + coordinatedElements[i].Opacity = originalOpacities[i]; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + OnAnimationComplete(); + } + + private async Task RunFallbackAnimationAsync( + Visual destination, IReadOnlyList coordinatedElements, + TopLevel topLevel, TimeSpan duration, Easing easing, + bool useGravityDip, bool useShadow) + { + var destTransform = destination.TransformToVisual(topLevel); + if (!destTransform.HasValue) { OnAnimationComplete(); return; } + + var destBounds = new Rect( + destTransform.Value.Transform(new Point(0, 0)), + new Size(destination.Bounds.Width, destination.Bounds.Height)); + + var dx = _sourceBounds.X - destBounds.X; + var dy = _sourceBounds.Y - destBounds.Y; + var sx = _sourceBounds.Width > 0 && destBounds.Width > 0 ? _sourceBounds.Width / destBounds.Width : 1.0; + var sy = _sourceBounds.Height > 0 && destBounds.Height > 0 ? _sourceBounds.Height / destBounds.Height : 1.0; + + var group = new TransformGroup(); + var scaleT = new ScaleTransform(sx, sy); + var transT = new TranslateTransform(dx, dy); + group.Children.Add(scaleT); + group.Children.Add(transT); + + var origTransform = destination.RenderTransform; + var origOrigin = destination.RenderTransformOrigin; + destination.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + destination.RenderTransform = group; + + double dipAmp = 0, scaleAmp = 0; + if (useGravityDip) + { + var travel = Math.Max(Math.Abs(dx), Math.Abs(dy)); + dipAmp = Math.Clamp(travel * 0.12, 8, 50); + scaleAmp = 0.05; + } + + var shadowBorder = useShadow && useGravityDip && destination is Border b ? b : null; + var origShadow = shadowBorder?.BoxShadow ?? default; + + var originalOpacities = new double[coordinatedElements.Count]; + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + originalOpacities[i] = coordinatedElements[i].Opacity; + coordinatedElements[i].Opacity = 0; + } + + var startTimestamp = Stopwatch.GetTimestamp(); + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + _animationTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + + _animationTimer.Tick += (_, _) => + { + var elapsed = Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds; + var progress = Math.Min(1.0, elapsed / duration.TotalMilliseconds); + var ep = easing.Ease(progress); + + var bsx = sx + (1.0 - sx) * ep; + var bsy = sy + (1.0 - sy) * ep; + var btx = dx * (1.0 - ep); + var bty = dy * (1.0 - ep); + + if (useGravityDip) + { + var dipCurve = Math.Sin(Math.PI * progress); + var scaleBoost = 1.0 + scaleAmp * dipCurve; + scaleT.ScaleX = bsx * scaleBoost; + scaleT.ScaleY = bsy * scaleBoost; + transT.X = btx; + transT.Y = bty + dipAmp * dipCurve; + + if (shadowBorder != null) + { + var alpha = (byte)(100 * dipCurve); + var blur = 24 * dipCurve; + var offsetY = 10 * dipCurve; + shadowBorder.BoxShadow = new BoxShadows(new BoxShadow + { + OffsetX = 0, OffsetY = offsetY, + Blur = blur, + Color = Color.FromArgb(alpha, 0, 0, 0) + }); + } + } + else + { + scaleT.ScaleX = bsx; + scaleT.ScaleY = bsy; + transT.X = btx; + transT.Y = bty; + } + + if (progress > CoordinatedFadeStartThreshold) + { + var cp = (progress - CoordinatedFadeStartThreshold) / CoordinatedFadeRange; + for (int j = 0; j < coordinatedElements.Count; j++) + { + if (ReferenceEquals(coordinatedElements[j], destination)) + continue; + coordinatedElements[j].Opacity = originalOpacities[j] * cp; + } + } + + if (progress >= 1.0) + { + _animationTimer!.Stop(); + _animationTimer = null; + tcs.TrySetResult(true); + } + }; + + _animationCts = new CancellationTokenSource(); + using var reg = _animationCts.Token.Register(() => tcs.TrySetCanceled()); + + _animationTimer.Start(); + + bool cancelled = false; + try + { + await tcs.Task; + } + catch (OperationCanceledException) + { + cancelled = true; + } + finally + { + _animationTimer?.Stop(); + _animationTimer = null; + _animationCts?.Dispose(); + _animationCts = null; + } + + destination.RenderTransform = origTransform; + destination.RenderTransformOrigin = origOrigin; + + if (shadowBorder != null) + shadowBorder.BoxShadow = origShadow; + + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + coordinatedElements[i].Opacity = originalOpacities[i]; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + if (cancelled) + { + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: true)); + return; + } + + OnAnimationComplete(); + } + + internal void ResolveTimingAndEasing(ConnectedAnimationService service, + out TimeSpan duration, out Easing easing, + out bool useGravityDip, out bool useShadow) + { + if (Configuration is DirectConnectedAnimationConfiguration direct) + { + duration = direct.Duration ?? service.DefaultDuration; + easing = s_directEasing; + useGravityDip = false; + useShadow = false; + } + else if (Configuration is BasicConnectedAnimationConfiguration) + { + duration = service.DefaultDuration; + easing = service.DefaultEasingFunction ?? s_basicEasing; + useGravityDip = false; + useShadow = false; + } + else + { + duration = service.DefaultDuration; + easing = service.DefaultEasingFunction ?? s_gravityEasing; + useGravityDip = true; + useShadow = Configuration is GravityConnectedAnimationConfiguration g + ? g.IsShadowEnabled + : true; + } + } + + private void OnAnimationComplete() + { + _service.RemoveAnimation(_key); + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: false)); + } + + private static IBrush? GetBackground(Visual visual) => visual switch + { + Border b => b.Background, + Panel p => p.Background, + ContentPresenter cp => cp.Background, + ContentControl cc => cc.Background, + TemplatedControl tc => tc.Background, + _ => null, + }; + + private static CornerRadius GetCornerRadius(Visual visual) => visual switch + { + Border b => b.CornerRadius, + TemplatedControl tc => tc.CornerRadius, + ContentPresenter cp => cp.CornerRadius, + _ => default, + }; + + private static Thickness GetBorderThickness(Visual visual) => visual switch + { + Border b => b.BorderThickness, + TemplatedControl tc => tc.BorderThickness, + ContentPresenter cp => cp.BorderThickness, + _ => default, + }; + + private static IBrush? GetBorderBrush(Visual visual) => visual switch + { + Border b => b.BorderBrush, + TemplatedControl tc => tc.BorderBrush, + ContentPresenter cp => cp.BorderBrush, + _ => null, + }; + + private static bool BrushesEqual(IBrush a, IBrush b) + { + if (ReferenceEquals(a, b)) return true; + if (a is ISolidColorBrush sa && b is ISolidColorBrush sb) + return sa.Color == sb.Color && Math.Abs(sa.Opacity - sb.Opacity) < 0.001; + return false; + } + + private class ConnectedAnimationProxy : Border + { + public static readonly StyledProperty ProgressProperty = + AvaloniaProperty.Register(nameof(Progress)); + + public double Progress + { + get => GetValue(ProgressProperty); + set => SetValue(ProgressProperty, value); + } + + internal Action? ProgressCallback { get; set; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ProgressProperty) + ProgressCallback?.Invoke(change.GetNewValue()); + } + } + } +} diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs b/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs new file mode 100644 index 0000000000..f4387aac44 --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs @@ -0,0 +1,56 @@ +using System; + +namespace Avalonia.Animation +{ + /// + /// Base class for connected animation configurations that control + /// the visual style and physics of the transition. + /// + internal abstract class ConnectedAnimationConfiguration + { + } + + /// + /// Produces a gravity-physics effect suitable for forward navigation: + /// the element arcs slightly as it travels and casts an animated shadow. + /// This is the default configuration when none is specified. + /// + /// + /// Use for back navigation + /// and for a plain transition. + /// + internal class GravityConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + /// + /// Gets or sets whether a drop shadow is rendered beneath the element + /// during the gravity arc. Defaults to . + /// + public bool IsShadowEnabled { get; set; } = true; + } + + /// + /// Produces a direct, linear translation suitable for back navigation. + /// No gravity arc or shadow is applied, and the default duration is shorter (150 ms). + /// + /// + /// Assign this to before calling + /// TryStart on the return animation to animate back to the source view. + /// + internal class DirectConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + /// + /// Gets or sets the duration of the animation. + /// When , is used. + /// + public TimeSpan? Duration { get; set; } + } + + /// + /// Produces a simple ease-in-out transition between the source and destination elements + /// with no gravity arc or shadow. Duration is taken from + /// . + /// + internal class BasicConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + } +} diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs b/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs new file mode 100644 index 0000000000..e132e5f22c --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Animation.Easings; +using Avalonia.Controls; + +namespace Avalonia.Animation +{ + /// + /// Coordinates connected animations across views. + /// Each window has its own independent instance so + /// animations cannot bleed across windows. + /// + /// + /// Typical usage: + /// + /// On the source view, call to capture the element. + /// Navigate to the destination view. + /// On the destination view, call then + /// TryStart on the returned animation to run the animation. + /// + /// + internal class ConnectedAnimationService : AvaloniaObject + { + private static readonly ConditionalWeakTable s_perView = new(); + private readonly Dictionary _animations = new(); + + internal ConnectedAnimationService() + { + DefaultDuration = TimeSpan.FromMilliseconds(300); + } + + /// + /// Gets the for the specified . + /// Each top-level window has its own isolated instance. + /// + public static ConnectedAnimationService GetForTopLevel(TopLevel topLevel) + { + ArgumentNullException.ThrowIfNull(topLevel); + return s_perView.GetValue(topLevel, static _ => new ConnectedAnimationService()); + } + + /// + /// Gets or sets the default duration applied to all animations whose + /// configuration does not specify one. Defaults to 300 ms. + /// + public TimeSpan DefaultDuration { get; set; } + + /// + /// Gets or sets the default easing function applied when the active + /// does not specify one. + /// When a configuration-specific default is used. + /// + public Easing? DefaultEasingFunction { get; set; } + + /// + /// Captures and registers a pending animation under + /// . Call this on the source view before navigating away. + /// + /// Unique string that pairs this call with the matching + /// call on the destination view. + /// The element to animate from. + /// The prepared . + public ConnectedAnimation PrepareToAnimate(string key, Visual source) + { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(source); + + // Replace any stale animation registered under the same key. + if (_animations.TryGetValue(key, out var old)) + { + _animations.Remove(key); + old.Dispose(); + } + + var animation = new ConnectedAnimation(key, source, this); + _animations[key] = animation; + return animation; + } + + /// + /// Retrieves a pending animation registered under . + /// Returns if no animation exists or if it has already been consumed. + /// Call this on the destination view after navigating, then call + /// TryStart on the returned animation. + /// + public ConnectedAnimation? GetAnimation(string key) + { + if (_animations.TryGetValue(key, out var animation) && !animation.IsConsumed) + return animation; + + return null; + } + + /// + /// Removes the animation registered under . + /// The caller is responsible for disposing the animation separately. + /// + internal void RemoveAnimation(string key) => _animations.Remove(key); + } +} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 9259be8594..bc299bdbe1 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -60,6 +60,16 @@ namespace Avalonia /// public string? RenderingSubsystemName { get; private set; } + /// + /// Gets or sets a method to call the initialize the text shaping subsystem. + /// + public Action? TextShapingSubsystemInitializer { get; private set; } + + /// + /// Gets the name of the currently selected text shaping subsystem. + /// + public string? TextShapingSubsystemName { get; private set; } + /// /// Gets a method to call after the is setup. /// @@ -224,6 +234,19 @@ namespace Avalonia RenderingSubsystemName = name; return Self; } + + /// + /// Specifies a text shaping subsystem to use. + /// + /// The method to call to initialize the text shaping subsystem. + /// The name of the text shaping subsystem. + /// An instance. + public AppBuilder UseTextShapingSubsystem(Action initializer, string name = "") + { + TextShapingSubsystemInitializer = initializer; + TextShapingSubsystemName = name; + return Self; + } /// /// Specifies a runtime platform subsystem to use. @@ -308,7 +331,12 @@ namespace Avalonia if (RenderingSubsystemInitializer == null) { - throw new InvalidOperationException("No rendering system configured."); + throw new InvalidOperationException("No rendering system configured. Consider calling UseSkia()."); + } + + if (TextShapingSubsystemInitializer == null) + { + throw new InvalidOperationException("No text shaping system configured. Consider calling UseHarfBuzz()."); } if (_appFactory == null) @@ -333,6 +361,7 @@ namespace Avalonia { _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer?.Invoke(); + TextShapingSubsystemInitializer?.Invoke(); RenderingSubsystemInitializer?.Invoke(); WindowingSubsystemInitializer?.Invoke(); AfterPlatformServicesSetupCallback?.Invoke(Self); diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index e46dcb0eb2..b8d22cb30a 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -93,6 +93,29 @@ namespace Avalonia.Automation "ControlTypeOverride", typeof(AutomationProperties)); + /// + /// Defines the AutomationProperties.ClassNameOverride attached property. + /// + /// + /// This property affects the default value for . + /// + public static readonly AttachedProperty ClassNameOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ClassNameOverride", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsControlElementOverride attached property. + /// + /// + /// This property affects the default value for + /// . + /// + public static readonly AttachedProperty IsControlElementOverrideProperty = + AvaloniaProperty.RegisterAttached( + "IsControlElementOverride", + typeof(AutomationProperties)); + /// /// Defines the AutomationProperties.HelpText attached property. /// @@ -352,6 +375,42 @@ namespace Avalonia.Automation return element.GetValue(ControlTypeOverrideProperty); } + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetClassNameOverride(StyledElement element, string? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(ClassNameOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static string? GetClassNameOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ClassNameOverrideProperty); + } + + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetIsControlElementOverride(StyledElement element, bool? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(IsControlElementOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static bool? GetIsControlElementOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsControlElementOverrideProperty); + } + /// /// Helper for setting the value of the on a StyledElement. /// diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index bbce6286ce..b6b056658b 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -206,7 +206,7 @@ namespace Avalonia.Automation.Peers /// /// /// - public string GetClassName() => GetClassNameCore() ?? string.Empty; + public string GetClassName() => GetClassNameOverrideCore() ?? string.Empty; /// /// Gets the automation peer for the label that is targeted to the element. @@ -646,6 +646,11 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } + protected virtual string GetClassNameOverrideCore() + { + return GetClassNameCore(); + } + private protected virtual AutomationPeer? GetAutomationRootCore() { var peer = this; diff --git a/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs new file mode 100644 index 0000000000..1c546aa7ca --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class ContentPageAutomationPeer : ControlAutomationPeer +{ + public ContentPageAutomationPeer(ContentPage owner) + : base(owner) + { + } + + public new ContentPage Owner => (ContentPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 2dd896791f..59bfc6a045 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -219,6 +219,11 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } + protected override string GetClassNameOverrideCore() + { + return AutomationProperties.GetClassNameOverride(Owner) ?? GetClassNameCore(); + } + protected override bool IsContentElementOverrideCore() { var view = AutomationProperties.GetAccessibilityView(Owner); @@ -227,6 +232,8 @@ namespace Avalonia.Automation.Peers protected override bool IsControlElementOverrideCore() { + if (AutomationProperties.GetIsControlElementOverride(Owner) is { } isControlElement) + return isControlElement; var view = AutomationProperties.GetAccessibilityView(Owner); return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control; } diff --git a/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs new file mode 100644 index 0000000000..35477ae7d4 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class DrawerPageAutomationPeer : ControlAutomationPeer +{ + public DrawerPageAutomationPeer(DrawerPage owner) + : base(owner) + { + } + + public new DrawerPage Owner => (DrawerPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs new file mode 100644 index 0000000000..ea7e03f7b4 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class NavigationPageAutomationPeer : ControlAutomationPeer +{ + public NavigationPageAutomationPeer(NavigationPage owner) + : base(owner) + { + } + + public new NavigationPage Owner => (NavigationPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + if (string.IsNullOrEmpty(result)) + result = Owner.CurrentPage?.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs new file mode 100644 index 0000000000..44c0b35af6 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class TabbedPageAutomationPeer : ControlAutomationPeer +{ + public TabbedPageAutomationPeer(TabbedPage owner) + : base(owner) + { + } + + public new TabbedPage Owner => (TabbedPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs deleted file mode 100644 index 4bd606dd6e..0000000000 --- a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Avalonia.Automation; -using Avalonia.Automation.Peers; -using Avalonia.Controls.Chrome; - -namespace Avalonia.Controls.Automation.Peers; - -internal class TitleBarAutomationPeer : ControlAutomationPeer -{ - public TitleBarAutomationPeer(TitleBar owner) : base(owner) - { - } - - protected override bool IsContentElementCore() => false; - - protected override string GetClassNameCore() - { - return "TitleBar"; - } - - protected override string? GetAutomationIdCore() => base.GetAutomationIdCore() ?? "AvaloniaTitleBar"; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.TitleBar; - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index 1162132d54..983b92313e 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; using Avalonia.Controls; namespace Avalonia.Automation.Peers @@ -19,12 +21,23 @@ namespace Avalonia.Automation.Peers protected override string? GetNameCore() => Owner.Title; + protected override IReadOnlyList? GetChildrenCore() + { + var baseChildren = base.GetChildrenCore(); + var overlayPeer = Owner.TopLevelHost.GetOrCreateDecorationsOverlaysPeer(); + + var rv = new List { overlayPeer }; + if (baseChildren?.Count > 0) + rv.AddRange(baseChildren); + return rv; + } + private void OnOpened(object? sender, EventArgs e) { Owner.Opened -= OnOpened; StartTrackingFocus(); } - + private void OnClosed(object? sender, EventArgs e) { Owner.Closed -= OnClosed; diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs index 4bbb2669d7..e745665bcf 100644 --- a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; namespace Avalonia.Automation.Provider { @@ -12,6 +13,7 @@ namespace Avalonia.Automation.Provider /// an automation tree from a 3rd party UI framework that wishes to use Avalonia's automation /// support. /// + [PrivateApi] public interface IEmbeddedRootProvider { /// diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index 8452574df2..77ad98cbd7 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Automation.Provider @@ -13,6 +14,7 @@ namespace Avalonia.Automation.Provider /// be implemented on true root elements, such as Windows. To embed an automation tree, use /// instead. /// + [PrivateApi] public interface IRootProvider { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 2bfe3c9be2..3d548e15b9 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -299,6 +299,19 @@ namespace Avalonia.Controls.Primitives } } + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Reset mouse button tracking state. When the calendar popup closes + // (e.g. due to a programmatic window change during date selection), + // the PointerReleased event never fires, leaving these flags stuck. + // See https://github.com/AvaloniaUI/Avalonia/issues/18418 + _isMouseLeftButtonDown = false; + _isMouseLeftButtonDownYearView = false; + } + private void SetDayTitles() { for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs deleted file mode 100644 index 32bdd8fa96..0000000000 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using Avalonia.Reactive; -using Avalonia.Controls.Metadata; -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls.Chrome -{ - /// - /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. - /// - [TemplatePart(PART_CloseButton, typeof(Button))] - [TemplatePart(PART_RestoreButton, typeof(Button))] - [TemplatePart(PART_MinimizeButton, typeof(Button))] - [TemplatePart(PART_FullScreenButton, typeof(Button))] - [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] - public class CaptionButtons : TemplatedControl - { - internal const string PART_CloseButton = "PART_CloseButton"; - internal const string PART_RestoreButton = "PART_RestoreButton"; - internal const string PART_MinimizeButton = "PART_MinimizeButton"; - internal const string PART_FullScreenButton = "PART_FullScreenButton"; - - private Button? _restoreButton; - private Button? _minimizeButton; - private Button? _fullScreenButton; - private IDisposable? _disposables; - - /// - /// Currently attached window. - /// - protected Window? HostWindow { get; private set; } - - public virtual void Attach(Window hostWindow) - { - if (_disposables == null) - { - HostWindow = hostWindow; - - _disposables = new CompositeDisposable - { - HostWindow.GetObservable(Window.CanMaximizeProperty) - .Subscribe(_ => - { - UpdateRestoreButtonState(); - UpdateFullScreenButtonState(); - }), - HostWindow.GetObservable(Window.CanMinimizeProperty) - .Subscribe(_ => - { - UpdateMinimizeButtonState(); - }), - HostWindow.GetObservable(Window.WindowStateProperty) - .Subscribe(x => - { - PseudoClasses.Set(":minimized", x == WindowState.Minimized); - PseudoClasses.Set(":normal", x == WindowState.Normal); - PseudoClasses.Set(":maximized", x == WindowState.Maximized); - PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - UpdateRestoreButtonState(); - UpdateMinimizeButtonState(); - UpdateFullScreenButtonState(); - }), - }; - } - } - - public virtual void Detach() - { - if (_disposables != null) - { - _disposables.Dispose(); - _disposables = null; - - HostWindow = null; - } - } - - protected virtual void OnClose() - { - HostWindow?.Close(); - } - - protected virtual void OnRestore() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - } - - protected virtual void OnMinimize() - { - if (HostWindow != null) - { - HostWindow.WindowState = WindowState.Minimized; - } - } - - protected virtual void OnToggleFullScreen() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen - ? WindowState.Normal - : WindowState.FullScreen; - } - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - - if (e.NameScope.Find