diff --git a/Avalonia.sln b/Avalonia.sln index 8690190678..4488dc3e44 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -207,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}" diff --git a/Directory.Packages.props b/Directory.Packages.props index 7e137dbea4..58650e1729 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,9 +12,9 @@ - - - + + + @@ -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 cd9dedbd0f..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 @@ -31,30 +43,66 @@ 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}) @@ -67,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}) @@ -109,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 a495938edc..92f41c6606 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -337,6 +337,36 @@ 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 @@ -385,6 +415,12 @@ 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 @@ -763,6 +799,36 @@ 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 @@ -811,6 +877,12 @@ 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 @@ -1045,6 +1117,126 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + 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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) @@ -1285,6 +1477,12 @@ 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) @@ -1327,6 +1525,24 @@ 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) @@ -1489,6 +1705,12 @@ 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 @@ -1597,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) @@ -1867,6 +2095,12 @@ 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) @@ -1885,6 +2119,12 @@ 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}) @@ -1897,6 +2137,12 @@ 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) @@ -1987,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 @@ -2223,49 +2517,169 @@ 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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.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/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) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.GetTextAsync + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll @@ -2461,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) @@ -2503,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) @@ -2665,6 +3103,12 @@ 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 @@ -2773,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) @@ -3043,6 +3493,12 @@ 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) @@ -3061,6 +3517,12 @@ 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}) @@ -3073,6 +3535,12 @@ 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) @@ -3169,18 +3637,66 @@ 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.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.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.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(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.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw 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 @@ -3223,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}) @@ -3241,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 @@ -3259,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@) @@ -3295,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 @@ -3331,18 +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}) @@ -3355,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) @@ -3445,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 @@ -3463,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@) @@ -3499,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) @@ -3511,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) @@ -3523,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 @@ -3547,12 +4219,24 @@ 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 @@ -3571,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}) @@ -3589,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) @@ -3685,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 @@ -3703,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 @@ -3853,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 @@ -3871,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 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/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/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/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/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/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/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 31c4d0c60c..47d95d8da1 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -410,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/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/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/InputElement.Gestures.cs b/src/Avalonia.Base/Input/InputElement.Gestures.cs index 25f6362d60..1ad1146282 100644 --- a/src/Avalonia.Base/Input/InputElement.Gestures.cs +++ b/src/Avalonia.Base/Input/InputElement.Gestures.cs @@ -19,45 +19,82 @@ namespace Avalonia.Input public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(InputElement), false); + /// + /// Defines the event. + /// public static readonly RoutedEvent PinchEvent = - RoutedEvent.Register( - "Pinch", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(Pinch), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PinchEndedEvent = - RoutedEvent.Register( - "PinchEnded", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(PinchEnded), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PullGestureEvent = - RoutedEvent.Register( - "PullGesture", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(PullGesture), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PullGestureEndedEvent = - RoutedEvent.Register( - "PullGestureEnded", RoutingStrategies.Bubble, typeof(InputElement)); + 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( - "ScrollGesture", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(ScrollGesture), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = - RoutedEvent.Register( - "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(ScrollGestureInertiaStarting), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(ScrollGestureEnded), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerTouchPadGestureMagnify", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(PointerTouchPadGestureMagnify), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerTouchPadGestureRotate", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(PointerTouchPadGestureRotate), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerTouchPadGestureSwipe", RoutingStrategies.Bubble, typeof(InputElement)); + RoutedEvent.Register( + nameof(PointerTouchPadGestureSwipe), RoutingStrategies.Bubble); /// /// Defines the event. @@ -95,6 +132,7 @@ namespace Avalonia.Input { return element.GetValue(IsHoldingEnabledProperty); } + public static void SetIsHoldingEnabled(StyledElement element, bool value) { element.SetValue(IsHoldingEnabledProperty, value); @@ -104,70 +142,110 @@ namespace Avalonia.Input { return element.GetValue(IsHoldWithMouseEnabledProperty); } + public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) { element.SetValue(IsHoldWithMouseEnabledProperty, value); } - 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 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 AddPointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureMagnifyEvent, handler); - - public static void AddPointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureRotateEvent, handler); + /// + /// Occurs when a pinch gesture occurs on the control. + /// + public event EventHandler? Pinch + { + add { AddHandler(PinchEvent, value); } + remove { RemoveHandler(PinchEvent, value); } + } - public static void AddPointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureSwipeEvent, handler); + /// + /// Occurs when a pinch gesture ends on the control. + /// + public event EventHandler? PinchEnded + { + add { AddHandler(PinchEndedEvent, value); } + remove { RemoveHandler(PinchEndedEvent, value); } + } - public static void AddScrollGestureHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEvent, handler); + /// + /// Occurs when a pull gesture occurs on the control. + /// + public event EventHandler? PullGesture + { + add { AddHandler(PullGestureEvent, value); } + remove { RemoveHandler(PullGestureEvent, value); } + } - public static void AddScrollGestureEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEndedEvent, handler); + /// + /// Occurs when a pull gesture ends on the control. + /// + public event EventHandler? PullGestureEnded + { + add { AddHandler(PullGestureEndedEvent, value); } + remove { RemoveHandler(PullGestureEndedEvent, value); } + } - public static void AddScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureInertiaStartingEvent, handler); + /// + /// Occurs when a scroll gesture occurs on the control. + /// + public event EventHandler? ScrollGesture + { + add { AddHandler(ScrollGestureEvent, value); } + remove { RemoveHandler(ScrollGestureEvent, value); } + } - public static void RemovePointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureMagnifyEvent, handler); + /// + /// Occurs when a scroll gesture inertia starts on the control. + /// + public event EventHandler? ScrollGestureInertiaStarting + { + add { AddHandler(ScrollGestureInertiaStartingEvent, value); } + remove { RemoveHandler(ScrollGestureInertiaStartingEvent, value); } + } - public static void RemovePointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureRotateEvent, handler); + /// + /// Occurs when a scroll gesture ends on the control. + /// + public event EventHandler? ScrollGestureEnded + { + add { AddHandler(ScrollGestureEndedEvent, value); } + remove { RemoveHandler(ScrollGestureEndedEvent, value); } + } - public static void RemovePointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureSwipeEvent, handler); + /// + /// Occurs when a touchpad magnify gesture occurs on the control. + /// + public event EventHandler? PointerTouchPadGestureMagnify + { + add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } + } - public static void RemoveScrollGestureHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureEvent, handler); + /// + /// Occurs when a touchpad rotate gesture occurs on the control. + /// + public event EventHandler? PointerTouchPadGestureRotate + { + add { AddHandler(PointerTouchPadGestureRotateEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } + } - public static void RemoveScrollGestureEndedHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureEndedEvent, handler); + /// + /// Occurs when a swipe gesture occurs on the control. + /// + public event EventHandler? SwipeGesture + { + add { AddHandler(SwipeGestureEvent, value); } + remove { RemoveHandler(SwipeGestureEvent, value); } + } - public static void RemoveScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureInertiaStartingEvent, handler); + /// + /// 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. 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/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/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.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 52c34cbc41..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,7 +14,7 @@ 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)); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 0c8656604a..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,7 +40,7 @@ namespace Avalonia.Rendering.Composition.Server public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; @@ -141,6 +142,8 @@ namespace Avalonia.Rendering.Composition.Server try { + if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces())) + return; _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); } catch (RenderTargetNotReadyException) @@ -160,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; @@ -203,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/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.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/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/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/CommandBar/AppBarButton.cs b/src/Avalonia.Controls/CommandBar/AppBarButton.cs new file mode 100644 index 0000000000..9e251f77bf --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarButton.cs @@ -0,0 +1,119 @@ +namespace Avalonia.Controls +{ + /// + /// A button for use in a . + /// + public class AppBarButton : Button, ICommandBarElement + { + static AppBarButton() + { + ForegroundProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + IconProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelProperty = + AvaloniaProperty.Register(nameof(Label)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DynamicOverflowOrderProperty = + AvaloniaProperty.Register(nameof(DynamicOverflowOrder)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelPositionProperty = + AvaloniaProperty.Register(nameof(LabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets the text label for the button. + /// + public string? Label + { + get => GetValue(LabelProperty); + set => SetValue(LabelProperty, value); + } + + /// + /// Gets or sets the icon content for the button. + /// + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets whether the button is in compact mode (icon only, label hidden). + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets the order in which this button moves to the overflow menu when space is limited. + /// Lower values have higher priority (stay visible longer). + /// + public int DynamicOverflowOrder + { + get => GetValue(DynamicOverflowOrderProperty); + set => SetValue(DynamicOverflowOrderProperty, value); + } + + /// + /// Gets or sets the label position. This is set automatically by the parent . + /// + public CommandBarDefaultLabelPosition LabelPosition + { + get => GetValue(LabelPositionProperty); + set => SetValue(LabelPositionProperty, value); + } + + /// + /// Gets or sets whether this button is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + + private void UpdateIconForeground() + { + if (Icon is IconElement icon) + { + var fg = Foreground; + + if (fg != null) + icon.SetValue(ForegroundProperty, fg); + else + icon.ClearValue(ForegroundProperty); + } + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs b/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs new file mode 100644 index 0000000000..53c114528f --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// A visual separator for use in a . + /// + public class AppBarSeparator : TemplatedControl, ICommandBarElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets whether the separator is in compact mode. + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets whether the separator is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs b/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs new file mode 100644 index 0000000000..2476e3cb93 --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs @@ -0,0 +1,121 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// A toggle button for use in a . + /// + public class AppBarToggleButton : ToggleButton, ICommandBarElement + { + static AppBarToggleButton() + { + ForegroundProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + IconProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelProperty = + AvaloniaProperty.Register(nameof(Label)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DynamicOverflowOrderProperty = + AvaloniaProperty.Register(nameof(DynamicOverflowOrder)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelPositionProperty = + AvaloniaProperty.Register(nameof(LabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets the text label for the button. + /// + public string? Label + { + get => GetValue(LabelProperty); + set => SetValue(LabelProperty, value); + } + + /// + /// Gets or sets the icon content for the button. + /// + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets whether the button is in compact mode (icon only, label hidden). + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets the order in which this button moves to the overflow menu when space is limited. + /// Lower values have higher priority (stay visible longer). + /// + public int DynamicOverflowOrder + { + get => GetValue(DynamicOverflowOrderProperty); + set => SetValue(DynamicOverflowOrderProperty, value); + } + + /// + /// Gets or sets the label position. This is set automatically by the parent . + /// + public CommandBarDefaultLabelPosition LabelPosition + { + get => GetValue(LabelPositionProperty); + set => SetValue(LabelPositionProperty, value); + } + + /// + /// Gets or sets whether this button is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + + private void UpdateIconForeground() + { + if (Icon is IconElement icon) + { + var fg = Foreground; + + if (fg != null) + icon.SetValue(ForegroundProperty, fg); + else + icon.ClearValue(ForegroundProperty); + } + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs new file mode 100644 index 0000000000..2f068cbb9f --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs @@ -0,0 +1,642 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + /// + /// A command bar that provides primary commands displayed inline and secondary commands + /// accessible via an overflow menu. + /// + [TemplatePart("PART_OverflowButton", typeof(Button))] + [TemplatePart("PART_OverflowPopup", typeof(Popup))] + [TemplatePart("PART_ContentPresenter", typeof(Control))] + public class CommandBar : TemplatedControl + { + /// + /// Defines the property. + /// + public static readonly StyledProperty?> PrimaryCommandsProperty = + AvaloniaProperty.Register?>(nameof(PrimaryCommands)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty?> SecondaryCommandsProperty = + AvaloniaProperty.Register?>(nameof(SecondaryCommands)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ContentProperty = + ContentControl.ContentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DefaultLabelPositionProperty = + AvaloniaProperty.Register(nameof(DefaultLabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsDynamicOverflowEnabledProperty = + AvaloniaProperty.Register(nameof(IsDynamicOverflowEnabled)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OverflowButtonVisibilityProperty = + AvaloniaProperty.Register(nameof(OverflowButtonVisibility), CommandBarOverflowButtonVisibility.Auto); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsStickyProperty = + AvaloniaProperty.Register(nameof(IsSticky)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthBottomProperty = + AvaloniaProperty.Register(nameof(ItemWidthBottom), defaultValue: 70d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthRightProperty = + AvaloniaProperty.Register(nameof(ItemWidthRight), defaultValue: 102d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthCollapsedProperty = + AvaloniaProperty.Register(nameof(ItemWidthCollapsed), defaultValue: 42d); + + private bool _hasSecondaryCommands; + /// + /// Defines the property. + /// + public static readonly DirectProperty HasSecondaryCommandsProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSecondaryCommands), + o => o._hasSecondaryCommands); + + private bool _isOverflowButtonVisible; + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOverflowButtonVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsOverflowButtonVisible), + o => o._isOverflowButtonVisible); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent OpeningEvent = + RoutedEvent.Register(nameof(Opening), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent OpenedEvent = + RoutedEvent.Register(nameof(Opened), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent ClosingEvent = + RoutedEvent.Register(nameof(Closing), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent ClosedEvent = + RoutedEvent.Register(nameof(Closed), RoutingStrategies.Bubble); + + private Button? _overflowButton; + private Popup? _overflowPopup; + private Control? _contentPresenter; + + private readonly ObservableCollection _visiblePrimaryCommands = new(); + private readonly ObservableCollection _overflowItems = new(); + private bool _isDynamicUpdateInProgress; + private double _constraintWidth = double.PositiveInfinity; + + public CommandBar() + { + VisiblePrimaryCommands = new ReadOnlyObservableCollection(_visiblePrimaryCommands); + OverflowItems = new ReadOnlyObservableCollection(_overflowItems); + + var primaryCommands = new ObservableCollection(); + primaryCommands.CollectionChanged += OnPrimaryCommandsChanged; + SetCurrentValue(PrimaryCommandsProperty, (IList)primaryCommands); + + var secondaryCommands = new ObservableCollection(); + secondaryCommands.CollectionChanged += OnSecondaryCommandsChanged; + SetCurrentValue(SecondaryCommandsProperty, (IList)secondaryCommands); + + SizeChanged += CommandBar_SizeChanged; + } + + /// + /// Gets the read-only collection of primary commands currently visible in the bar + /// (may be a subset when dynamic overflow is active). + /// + public ReadOnlyObservableCollection VisiblePrimaryCommands { get; } + + /// + /// Gets the read-only collection of items shown in the overflow popup (secondary commands + /// plus any primary commands moved to overflow by dynamic overflow). + /// + public ReadOnlyObservableCollection OverflowItems { get; } + + /// + /// Gets or sets the collection of primary commands displayed in the bar. + /// + [Content] + [SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "Necessary for now to avoid returning null.")] + public IList PrimaryCommands + { + get + { + var value = GetValue(PrimaryCommandsProperty); + if (value is null) + { + var list = new ObservableCollection(); + SetCurrentValue(PrimaryCommandsProperty, (IList)list); + return list; + } + return value; + } + set => SetValue(PrimaryCommandsProperty, value); + } + + /// + /// Gets or sets the collection of secondary commands shown in the overflow menu. + /// + [SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "Necessary for now to avoid returning null.")] + public IList SecondaryCommands + { + get + { + var value = GetValue(SecondaryCommandsProperty); + if (value is null) + { + var list = new ObservableCollection(); + SetCurrentValue(SecondaryCommandsProperty, (IList)list); + return list; + } + return value; + } + set => SetValue(SecondaryCommandsProperty, value); + } + + /// + /// Gets or sets custom content displayed at the start (left) of the bar. + /// + public object? Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// Gets or sets how labels are positioned on command buttons. + /// + public CommandBarDefaultLabelPosition DefaultLabelPosition + { + get => GetValue(DefaultLabelPositionProperty); + set => SetValue(DefaultLabelPositionProperty, value); + } + + /// + /// Gets or sets whether primary commands are automatically moved to the overflow menu + /// when there is not enough space to display them. + /// + public bool IsDynamicOverflowEnabled + { + get => GetValue(IsDynamicOverflowEnabledProperty); + set => SetValue(IsDynamicOverflowEnabledProperty, value); + } + + /// + /// Gets or sets the visibility of the overflow button. + /// + public CommandBarOverflowButtonVisibility OverflowButtonVisibility + { + get => GetValue(OverflowButtonVisibilityProperty); + set => SetValue(OverflowButtonVisibilityProperty, value); + } + + /// + /// Gets or sets whether the overflow menu is open. + /// + public bool IsOpen + { + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + /// + /// Gets or sets whether the overflow menu stays open after a command is invoked + /// (disables light-dismiss behavior). + /// + public bool IsSticky + { + get => GetValue(IsStickyProperty); + set => SetValue(IsStickyProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthBottom + { + get => GetValue(ItemWidthBottomProperty); + set => SetValue(ItemWidthBottomProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthRight + { + get => GetValue(ItemWidthRightProperty); + set => SetValue(ItemWidthRightProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthCollapsed + { + get => GetValue(ItemWidthCollapsedProperty); + set => SetValue(ItemWidthCollapsedProperty, value); + } + + /// + /// Gets whether there are any commands (secondary or overflowed primary) in the overflow menu. + /// + public bool HasSecondaryCommands + { + get => _hasSecondaryCommands; + private set => SetAndRaise(HasSecondaryCommandsProperty, ref _hasSecondaryCommands, value); + } + + /// + /// Gets whether the overflow button is currently visible. + /// + public bool IsOverflowButtonVisible + { + get => _isOverflowButtonVisible; + private set => SetAndRaise(IsOverflowButtonVisibleProperty, ref _isOverflowButtonVisible, value); + } + + /// + /// Occurs when the overflow menu is about to open. + /// + public event EventHandler? Opening + { + add => AddHandler(OpeningEvent, value); + remove => RemoveHandler(OpeningEvent, value); + } + + /// + /// Occurs when the overflow menu has opened. + /// + public event EventHandler? Opened + { + add => AddHandler(OpenedEvent, value); + remove => RemoveHandler(OpenedEvent, value); + } + + /// + /// Occurs when the overflow menu is about to close. + /// + public event EventHandler? Closing + { + add => AddHandler(ClosingEvent, value); + remove => RemoveHandler(ClosingEvent, value); + } + + /// + /// Occurs when the overflow menu has closed. + /// + public event EventHandler? Closed + { + add => AddHandler(ClosedEvent, value); + remove => RemoveHandler(ClosedEvent, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (_overflowButton != null) + _overflowButton.Click -= OnOverflowButtonClick; + + _overflowButton = e.NameScope.Find