From 3553eda8ae690b553f529ca4b40d250cdd5784fa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 3 Mar 2026 18:58:42 +0500 Subject: [PATCH] Themeable Client Window Decorations (#20770) * Implemented new drawn window decorations API TODO: check if it works on Win32, bring back titlebar automation peer * Adjusting naming a bit * Naming / configuration changes * Various fixes * popover fix? * wip * Address review * Extra window roles * WIP * Fixed drawn titlebar automation * Purge ExtendClientAreaChromeHints. * Fixed dynamically enabling drawn decorations * api diff * Add automation IDs for drawn decorations buttons * Resolved the issues * build * Retry a few times when Pager isn't available after test is finished * Only do faulty test detection if asked * duplicate package reference * Try disabling faulty tests on appium1 * Fix ExtendClientAreaWindowTests * Apply initial button states * Enable CSD shadow for X11 * net8? * Address review * more review comments * Moar review comments * Extra hit-test checks * Moar review * Prefix integration test app exitfullscreen to avoid clashes * Disable drawn decorations if parts = None * Respect SystemDecorations value on mac in extend-client-area mode * Tidy up logic a bit * Adjust win32 tests to titlebar not being in the tree when CSD are not enabled --------- Co-authored-by: Julien Lebosquain --- api/Avalonia.nupkg.xml | 100 ++- native/Avalonia.Native/src/OSX/WindowImpl.h | 3 - native/Avalonia.Native/src/OSX/WindowImpl.mm | 44 +- samples/ControlCatalog/MainWindow.xaml | 1 - .../Pages/WindowCustomizationsPage.xaml | 9 - .../ViewModels/MainWindowViewModel.cs | 55 +- .../Pages/WindowDecorationsPage.axaml | 3 - .../Pages/WindowDecorationsPage.axaml.cs | 5 - .../IntegrationTestApp/Pages/WindowPage.axaml | 2 +- src/Avalonia.Base/Input/IInputRoot.cs | 12 + .../Input/WindowDecorationsElementRole.cs | 100 +++ .../Metadata/PrivateApiAttribute.cs | 2 +- .../Automation/AutomationProperties.cs | 59 ++ .../Automation/Peers/AutomationPeer.cs | 7 +- .../Automation/Peers/ControlAutomationPeer.cs | 7 + .../Peers/TitleBarAutomationPeer.cs | 26 - .../Automation/Peers/WindowAutomationPeer.cs | 15 +- .../Provider/IEmbeddedRootProvider.cs | 2 + .../Automation/Provider/IRootProvider.cs | 2 + .../Chrome/CaptionButtons.cs | 187 ------ .../Chrome/DrawnWindowDecorationParts.cs | 41 ++ .../Chrome/IWindowDrawnDecorationsTemplate.cs | 20 + .../Chrome/ResizeGripLayer.cs | 106 ++++ src/Avalonia.Controls/Chrome/TitleBar.cs | 111 ---- .../Chrome/WindowDecorationProperties.cs | 27 + .../Chrome/WindowDrawnDecorations.cs | 590 ++++++++++++++++++ .../Chrome/WindowDrawnDecorationsContent.cs | 59 ++ .../Platform/ExtendClientAreaChromeHints.cs | 38 -- src/Avalonia.Controls/Platform/IWindowImpl.cs | 13 +- .../PlatformRequestedDrawnDecoration.cs | 33 + .../PresentationSource/PresentationSource.cs | 37 ++ .../Primitives/ChromeOverlayLayer.cs | 11 - .../Primitives/VisualLayerManager.cs | 38 -- src/Avalonia.Controls/TopLevel.cs | 4 + .../TopLevelHost.Decorations.cs | 252 ++++++++ src/Avalonia.Controls/TopLevelHost.Peers.cs | 98 +++ src/Avalonia.Controls/TopLevelHost.cs | 14 +- src/Avalonia.Controls/Window.cs | 147 ++++- .../Remote/PreviewerWindowImpl.cs | 5 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 8 +- src/Avalonia.Native/WindowImpl.cs | 19 +- src/Avalonia.Native/avn.idl | 10 - .../Accents/FluentControlResources.xaml | 4 +- .../Controls/CaptionButtons.xaml | 119 ---- .../Controls/FluentControls.xaml | 3 +- .../Controls/TitleBar.xaml | 62 -- .../Controls/WindowDrawnDecorations.xaml | 215 +++++++ src/Avalonia.Themes.Simple/Accents/Base.xaml | 4 +- .../Controls/CaptionButtons.xaml | 126 ---- .../Controls/SimpleControls.xaml | 3 +- .../Controls/TitleBar.xaml | 69 -- .../Controls/WindowDrawnDecorations.xaml | 221 +++++++ src/Avalonia.X11/X11Platform.cs | 22 +- src/Avalonia.X11/X11Window.cs | 83 ++- .../Avalonia.Headless/HeadlessWindowImpl.cs | 7 +- .../Avalonia.Markup.Xaml.csproj | 1 + .../WindowDrawnDecorationsTemplate.cs | 21 + .../WindowImpl.CustomCaptionProc.cs | 32 + src/Windows/Avalonia.Win32/WindowImpl.cs | 26 +- .../ElementExtensions.cs | 12 +- .../PointerTests_MacOS.cs | 87 +-- .../TestBase.cs | 25 +- .../TrayIconTests.cs | 5 +- .../WindowDecorationsTests.cs | 50 +- .../WindowTests_MacOS.cs | 10 +- .../ExtendClientAreaWindowTests.cs | 34 +- 66 files changed, 2417 insertions(+), 1146 deletions(-) create mode 100644 src/Avalonia.Base/Input/WindowDecorationsElementRole.cs delete mode 100644 src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs delete mode 100644 src/Avalonia.Controls/Chrome/CaptionButtons.cs create mode 100644 src/Avalonia.Controls/Chrome/DrawnWindowDecorationParts.cs create mode 100644 src/Avalonia.Controls/Chrome/IWindowDrawnDecorationsTemplate.cs create mode 100644 src/Avalonia.Controls/Chrome/ResizeGripLayer.cs delete mode 100644 src/Avalonia.Controls/Chrome/TitleBar.cs create mode 100644 src/Avalonia.Controls/Chrome/WindowDecorationProperties.cs create mode 100644 src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs create mode 100644 src/Avalonia.Controls/Chrome/WindowDrawnDecorationsContent.cs delete mode 100644 src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs create mode 100644 src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs delete mode 100644 src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs create mode 100644 src/Avalonia.Controls/TopLevelHost.Decorations.cs create mode 100644 src/Avalonia.Controls/TopLevelHost.Peers.cs delete mode 100644 src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml delete mode 100644 src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml delete mode 100644 src/Avalonia.Themes.Simple/Controls/TitleBar.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml create mode 100644 src/Markup/Avalonia.Markup.Xaml/Templates/WindowDrawnDecorationsTemplate.cs diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index a1656e6527..a495938edc 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -253,6 +253,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Chrome.CaptionButtons + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Chrome.TitleBar + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.ContextRequestedEventArgs @@ -379,6 +391,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Platform.IApplicationPlatformEvents @@ -661,6 +679,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Chrome.CaptionButtons + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Chrome.TitleBar + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.ContextRequestedEventArgs @@ -787,6 +817,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Platform.IApplicationPlatformEvents @@ -1447,6 +1483,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.AppBuilder.get_LifetimeOverride @@ -1819,6 +1861,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) @@ -1837,6 +1891,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) @@ -2599,6 +2659,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.AppBuilder.get_LifetimeOverride @@ -2971,6 +3037,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) @@ -2989,6 +3067,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) @@ -3253,6 +3337,12 @@ 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.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) @@ -3463,6 +3553,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) @@ -4117,4 +4213,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - + \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 940699f09d..353ee4d2f1 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -62,8 +62,6 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetExtendClientArea (bool enable) override; - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override; - virtual HRESULT GetExtendTitleBarHeight (double*ret) override; virtual HRESULT SetExtendTitleBarHeight (double value) override; @@ -110,7 +108,6 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; - AvnExtendClientAreaChromeHints _extendClientHints; bool _isModal; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 5a57715b55..1d77d3ce44 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -13,7 +13,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowBaseImpl(events, false) { _isEnabled = true; _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _canMinimize = true; @@ -153,20 +152,11 @@ void WindowImpl::WindowStateChanged() { if (_isClientAreaExtended) { if (_lastWindowState == FullScreen) { // we exited fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - [Window setTitlebarAppearsTransparent:true]; [StandardContainer setFrameSize:StandardContainer.frame.size]; } else if (state == FullScreen) { // we entered fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = nullptr; - } - [Window setTitlebarAppearsTransparent:false]; [StandardContainer setFrameSize:StandardContainer.frame.size]; @@ -240,6 +230,10 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateAppearance(); + if (_isClientAreaExtended) { + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; + } + switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -391,20 +385,9 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { [Window setTitlebarAppearsTransparent:true]; - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) { - [StandardContainer ShowTitleBar:true]; - } else { - [StandardContainer ShowTitleBar:false]; - } + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } else { - Window.toolbar = nullptr; - } + Window.toolbar = nullptr; } else { Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; @@ -420,17 +403,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } } -HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { - START_COM_CALL; - - @autoreleasepool { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } -} - HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { START_COM_CALL; @@ -619,9 +591,7 @@ void WindowImpl::UpdateAppearance() { return; } - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = (_decorations == SystemDecorationsFull) && - (_isClientAreaExtended ? wantsChrome : true); + bool hasTrafficLights = (_decorations == SystemDecorationsFull); NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ebf6ed9a0f..b466315878 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,6 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels;assembly=ControlCatalog" xmlns:v="using:ControlCatalog.Views" ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" - ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" CanResize="{Binding CanResize}" CanMinimize="{Binding CanMinimize}" diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index a647d34356..df84c0bd6b 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -23,22 +23,13 @@ - - - - diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 716675f2c6..ce9209f61a 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Dialogs; -using Avalonia.Platform; using System; using System.ComponentModel.DataAnnotations; using Avalonia; @@ -13,10 +12,7 @@ namespace ControlCatalog.ViewModels { private WindowState _windowState; private WindowState[] _windowStates = Array.Empty(); - private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private bool _extendClientAreaEnabled; - private bool _systemTitleBarEnabled; - private bool _preferSystemChromeEnabled; private double _titleBarHeight; private bool _isSystemBarVisible; private bool _displayEdgeToEdge; @@ -51,65 +47,16 @@ namespace ControlCatalog.ViewModels WindowState.FullScreen, }; - PropertyChanged += (s, e) => - { - if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled)) - { - var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; - - if (SystemTitleBarEnabled) - { - hints |= ExtendClientAreaChromeHints.SystemChrome; - } - if (PreferSystemChromeEnabled) - { - hints |= ExtendClientAreaChromeHints.PreferSystemChrome; - } - ChromeHints = hints; - } - }; - - SystemTitleBarEnabled = true; TitleBarHeight = -1; CanResize = true; CanMinimize = true; CanMaximize = true; } - public ExtendClientAreaChromeHints ChromeHints - { - get { return _chromeHints; } - set { RaiseAndSetIfChanged(ref _chromeHints, value); } - } - public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value) - { - SystemTitleBarEnabled = true; - } - } - } - - public bool SystemTitleBarEnabled - { - get { return _systemTitleBarEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value) - { - TitleBarHeight = -1; - } - } - } - - public bool PreferSystemChromeEnabled - { - get { return _preferSystemChromeEnabled; } - set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } } public double TitleBarHeight diff --git a/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml b/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml index 0a7dce9c02..05edf1d09b 100644 --- a/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml +++ b/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml @@ -6,9 +6,6 @@ x:Class="IntegrationTestApp.Pages.WindowDecorationsPage"> - - - - + diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs index 513ecb0fae..13b7bae813 100644 --- a/src/Avalonia.Base/Input/IInputRoot.cs +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -33,5 +33,17 @@ namespace Avalonia.Input // It's also currently used by automation since we have special WindowAutomationPeer which needs to target the // window itself public InputElement FocusRoot { get; } + + /// + /// Performs a hit-test for chrome/decoration elements at the given position. + /// + /// The point in root-relative coordinates. + /// + /// null if no chrome element was hit (no chrome involvement at this point). + /// or + /// if an interactive chrome element was hit — the platform should redirect non-client input to regular client input. + /// Any other non- value indicates a specific non-client role (titlebar, resize grip, etc.). + /// + internal WindowDecorationsElementRole? HitTestChromeElement(Point point) => null; } } diff --git a/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs new file mode 100644 index 0000000000..faa0922c33 --- /dev/null +++ b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs @@ -0,0 +1,100 @@ +namespace Avalonia.Input; + +/// +/// Defines the cross-platform role of a visual element for non-client hit-testing. +/// Used to mark elements as titlebar drag areas, resize grips, etc. +/// +public enum WindowDecorationsElementRole +{ + /// + /// No special role. The element is invisible to chrome hit-testing. + /// + None, + + /// + /// An interactive element that is part of the decorations chrome (e.g., a caption button). + /// Set by themes on decoration template elements. Input is passed through to the element + /// rather than being intercepted for non-client actions. + /// + DecorationsElement, + + /// + /// An interactive element set by user code that should receive input even when + /// overlapping chrome areas. Has the same effect as + /// but is intended for use by application developers. + /// + User, + + /// + /// The element acts as a titlebar drag area. + /// Clicking and dragging on this element initiates a platform window move. + /// + TitleBar, + + /// + /// Resize grip for the north (top) edge. + /// + ResizeN, + + /// + /// Resize grip for the south (bottom) edge. + /// + ResizeS, + + /// + /// Resize grip for the east (right) edge. + /// + ResizeE, + + /// + /// Resize grip for the west (left) edge. + /// + ResizeW, + + /// + /// Resize grip for the northeast corner. + /// + ResizeNE, + + /// + /// Resize grip for the northwest corner. + /// + ResizeNW, + + /// + /// Resize grip for the southeast corner. + /// + ResizeSE, + + /// + /// Resize grip for the southwest corner. + /// + ResizeSW, + + /// + /// The element acts as the window close button. + /// On Win32, maps to HTCLOSE for system close behavior. + /// On other platforms, treated as an interactive decoration element. + /// + CloseButton, + + /// + /// The element acts as the window minimize button. + /// On Win32, maps to HTMINBUTTON for system minimize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MinimizeButton, + + /// + /// The element acts as the window maximize/restore button. + /// On Win32, maps to HTMAXBUTTON for system maximize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MaximizeButton, + + /// + /// The element acts as the window fullscreen toggle button. + /// Treated as an interactive decoration element on all platforms. + /// + FullScreenButton +} diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs index 8246d4a18d..9685ed3817 100644 --- a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Metadata; -[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Struct)] public sealed class PrivateApiAttribute : Attribute { diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index e46dcb0eb2..b8d22cb30a 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -93,6 +93,29 @@ namespace Avalonia.Automation "ControlTypeOverride", typeof(AutomationProperties)); + /// + /// Defines the AutomationProperties.ClassNameOverride attached property. + /// + /// + /// This property affects the default value for . + /// + public static readonly AttachedProperty ClassNameOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ClassNameOverride", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsControlElementOverride attached property. + /// + /// + /// This property affects the default value for + /// . + /// + public static readonly AttachedProperty IsControlElementOverrideProperty = + AvaloniaProperty.RegisterAttached( + "IsControlElementOverride", + typeof(AutomationProperties)); + /// /// Defines the AutomationProperties.HelpText attached property. /// @@ -352,6 +375,42 @@ namespace Avalonia.Automation return element.GetValue(ControlTypeOverrideProperty); } + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetClassNameOverride(StyledElement element, string? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(ClassNameOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static string? GetClassNameOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ClassNameOverrideProperty); + } + + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetIsControlElementOverride(StyledElement element, bool? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(IsControlElementOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static bool? GetIsControlElementOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsControlElementOverrideProperty); + } + /// /// Helper for setting the value of the on a StyledElement. /// diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index bbce6286ce..b6b056658b 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -206,7 +206,7 @@ namespace Avalonia.Automation.Peers /// /// /// - public string GetClassName() => GetClassNameCore() ?? string.Empty; + public string GetClassName() => GetClassNameOverrideCore() ?? string.Empty; /// /// Gets the automation peer for the label that is targeted to the element. @@ -646,6 +646,11 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } + protected virtual string GetClassNameOverrideCore() + { + return GetClassNameCore(); + } + private protected virtual AutomationPeer? GetAutomationRootCore() { var peer = this; diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 2dd896791f..59bfc6a045 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -219,6 +219,11 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } + protected override string GetClassNameOverrideCore() + { + return AutomationProperties.GetClassNameOverride(Owner) ?? GetClassNameCore(); + } + protected override bool IsContentElementOverrideCore() { var view = AutomationProperties.GetAccessibilityView(Owner); @@ -227,6 +232,8 @@ namespace Avalonia.Automation.Peers protected override bool IsControlElementOverrideCore() { + if (AutomationProperties.GetIsControlElementOverride(Owner) is { } isControlElement) + return isControlElement; var view = AutomationProperties.GetAccessibilityView(Owner); return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control; } diff --git a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs deleted file mode 100644 index 4bd606dd6e..0000000000 --- a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Avalonia.Automation; -using Avalonia.Automation.Peers; -using Avalonia.Controls.Chrome; - -namespace Avalonia.Controls.Automation.Peers; - -internal class TitleBarAutomationPeer : ControlAutomationPeer -{ - public TitleBarAutomationPeer(TitleBar owner) : base(owner) - { - } - - protected override bool IsContentElementCore() => false; - - protected override string GetClassNameCore() - { - return "TitleBar"; - } - - protected override string? GetAutomationIdCore() => base.GetAutomationIdCore() ?? "AvaloniaTitleBar"; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.TitleBar; - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index 1162132d54..983b92313e 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; using Avalonia.Controls; namespace Avalonia.Automation.Peers @@ -19,12 +21,23 @@ namespace Avalonia.Automation.Peers protected override string? GetNameCore() => Owner.Title; + protected override IReadOnlyList? GetChildrenCore() + { + var baseChildren = base.GetChildrenCore(); + var overlayPeer = Owner.TopLevelHost.GetOrCreateDecorationsOverlaysPeer(); + + var rv = new List { overlayPeer }; + if (baseChildren?.Count > 0) + rv.AddRange(baseChildren); + return rv; + } + private void OnOpened(object? sender, EventArgs e) { Owner.Opened -= OnOpened; StartTrackingFocus(); } - + private void OnClosed(object? sender, EventArgs e) { Owner.Closed -= OnClosed; diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs index 4bbb2669d7..e745665bcf 100644 --- a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; namespace Avalonia.Automation.Provider { @@ -12,6 +13,7 @@ namespace Avalonia.Automation.Provider /// an automation tree from a 3rd party UI framework that wishes to use Avalonia's automation /// support. /// + [PrivateApi] public interface IEmbeddedRootProvider { /// diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index 8452574df2..77ad98cbd7 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Automation.Provider @@ -13,6 +14,7 @@ namespace Avalonia.Automation.Provider /// be implemented on true root elements, such as Windows. To embed an automation tree, use /// instead. /// + [PrivateApi] public interface IRootProvider { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs deleted file mode 100644 index 32bdd8fa96..0000000000 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using Avalonia.Reactive; -using Avalonia.Controls.Metadata; -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls.Chrome -{ - /// - /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. - /// - [TemplatePart(PART_CloseButton, typeof(Button))] - [TemplatePart(PART_RestoreButton, typeof(Button))] - [TemplatePart(PART_MinimizeButton, typeof(Button))] - [TemplatePart(PART_FullScreenButton, typeof(Button))] - [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] - public class CaptionButtons : TemplatedControl - { - internal const string PART_CloseButton = "PART_CloseButton"; - internal const string PART_RestoreButton = "PART_RestoreButton"; - internal const string PART_MinimizeButton = "PART_MinimizeButton"; - internal const string PART_FullScreenButton = "PART_FullScreenButton"; - - private Button? _restoreButton; - private Button? _minimizeButton; - private Button? _fullScreenButton; - private IDisposable? _disposables; - - /// - /// Currently attached window. - /// - protected Window? HostWindow { get; private set; } - - public virtual void Attach(Window hostWindow) - { - if (_disposables == null) - { - HostWindow = hostWindow; - - _disposables = new CompositeDisposable - { - HostWindow.GetObservable(Window.CanMaximizeProperty) - .Subscribe(_ => - { - UpdateRestoreButtonState(); - UpdateFullScreenButtonState(); - }), - HostWindow.GetObservable(Window.CanMinimizeProperty) - .Subscribe(_ => - { - UpdateMinimizeButtonState(); - }), - HostWindow.GetObservable(Window.WindowStateProperty) - .Subscribe(x => - { - PseudoClasses.Set(":minimized", x == WindowState.Minimized); - PseudoClasses.Set(":normal", x == WindowState.Normal); - PseudoClasses.Set(":maximized", x == WindowState.Maximized); - PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - UpdateRestoreButtonState(); - UpdateMinimizeButtonState(); - UpdateFullScreenButtonState(); - }), - }; - } - } - - public virtual void Detach() - { - if (_disposables != null) - { - _disposables.Dispose(); - _disposables = null; - - HostWindow = null; - } - } - - protected virtual void OnClose() - { - HostWindow?.Close(); - } - - protected virtual void OnRestore() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - } - - protected virtual void OnMinimize() - { - if (HostWindow != null) - { - HostWindow.WindowState = WindowState.Minimized; - } - } - - protected virtual void OnToggleFullScreen() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen - ? WindowState.Normal - : WindowState.FullScreen; - } - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - - if (e.NameScope.Find bool NeedsManagedDecorations { get; } + /// + /// Gets flags indicating which drawn decoration parts the platform requires. + /// For example, X11 needs shadow, border, and resize grips; Win32 only needs titlebar/buttons. + /// + PlatformRequestedDrawnDecoration RequestedDrawnDecorations { get; } + /// /// Gets a thickness that describes the amount each side of the non-client area extends into the client area. /// It includes the titlebar. @@ -142,12 +149,6 @@ namespace Avalonia.Platform /// true to enable, false to disable void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint); - /// - /// Sets hints that configure how the client area extends. - /// - /// - void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints); - /// /// Sets how big the non-client titlebar area should be. /// diff --git a/src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs b/src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs new file mode 100644 index 0000000000..08a1f14203 --- /dev/null +++ b/src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Controls.Platform; + +/// +/// Flags indicating which drawn decoration parts a platform backend requires. +/// +[Flags, PrivateApi] +public enum PlatformRequestedDrawnDecoration +{ + None = 0, + + /// + /// Platform needs app-drawn window shadow. + /// + Shadow = 1, + + /// + /// Platform needs app-drawn window border/frame. + /// + Border = 2, + + /// + /// Platform needs app-drawn resize grips. + /// + ResizeGrips = 4, + + /// + /// Platform needs app-drawn window titlebar. + /// + TitleBar = 8, +} diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index 85d5e38c12..c98a380640 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Chrome; using Avalonia.Input; using Avalonia.Input.TextInput; using Avalonia.Layout; @@ -6,6 +7,7 @@ using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; namespace Avalonia.Controls; @@ -115,4 +117,39 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi { return _pointerOverPreProcessor?.LastPosition; } + + private static bool ChromeHitTestFilter(Visual visual) + { + if (visual is not { IsVisible: true, IsAttachedToVisualTree: true } + or not IInputElement { IsEffectivelyVisible: true, IsHitTestVisible: true }) + return false; + + // Allow traversal into any container that might contain chrome elements + return true; + } + + private static WindowDecorationsElementRole? GetChromeRoleFromVisual(Visual? visual) + { + while (visual != null) + { + var role = Chrome.WindowDecorationProperties.GetElementRole(visual); + if (role != WindowDecorationsElementRole.None) + return role; + visual = visual.VisualParent; + } + return null; + } + + WindowDecorationsElementRole? IInputRoot.HitTestChromeElement(Point point) + { + // Check all visuals at the point (not just topmost) because chrome elements + // may be in the underlay layer which sits below the TopLevel in the visual tree. + foreach (var visual in RootVisual.GetVisualsAt(point, ChromeHitTestFilter)) + { + var role = GetChromeRoleFromVisual(visual); + if (role != null) + return role; + } + return null; + } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs deleted file mode 100644 index d925a7a70c..0000000000 --- a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Linq; -using Avalonia.Rendering; -using Avalonia.VisualTree; - -namespace Avalonia.Controls.Primitives -{ - internal class ChromeOverlayLayer : Panel - { - - } -} diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 18a50fb5c0..932cd033a3 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Controls.Chrome; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -7,7 +6,6 @@ namespace Avalonia.Controls.Primitives public sealed class VisualLayerManager : Decorator { private const int AdornerZIndex = int.MaxValue - 100; - private const int ChromeZIndex = int.MaxValue - 99; private const int LightDismissOverlayZIndex = int.MaxValue - 98; private const int OverlayZIndex = int.MaxValue - 97; private const int TextSelectorLayerZIndex = int.MaxValue - 96; @@ -28,42 +26,6 @@ namespace Avalonia.Controls.Primitives } } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - EnsureChromeOverlayIfNeeded(); - - base.OnAttachedToVisualTree(e); - } - - void EnsureChromeOverlayIfNeeded() - { - // HACK: This is a replacement hack for the old set of hacks for TitleBar. - - // Check if we are attached direclty-ish to a Window (i. e. no other VisualLayerManager in between). - // If we are, then we are the "main" VisualLayerManager and should create the ChromeOverlayLayer and add titlebar there - var parent = VisualParent; - while (parent != null) - { - if(parent is VisualLayerManager) - break; - else if (parent is Window window) - { - if (FindLayer() == null) - { - var layer = new ChromeOverlayLayer(); - AddLayer(layer, ChromeZIndex); - layer.Children.Add(new TitleBar()); - } - - break; - } - - parent = parent.VisualParent; - - } - } - internal OverlayLayer? OverlayLayer { get diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 2dd5c7bc03..8556d03d91 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -129,6 +129,8 @@ namespace Avalonia.Controls private IStorageProvider? _storageProvider; private Screens? _screens; private readonly PresentationSource _source; + private readonly TopLevelHost _topLevelHost; + internal TopLevelHost TopLevelHost => _topLevelHost; internal new PresentationSource PresentationSource => _source; internal IInputRoot InputRoot => _source; @@ -191,7 +193,9 @@ namespace Avalonia.Controls dependencyResolver ??= AvaloniaLocator.Current; var hostVisual = new TopLevelHost(this); + _topLevelHost = hostVisual; ((ISetLogicalParent)hostVisual).SetParent(this); + LogicalChildren.Add(hostVisual); _source = new PresentationSource(hostVisual, this, impl, dependencyResolver, () => ClientSize); diff --git a/src/Avalonia.Controls/TopLevelHost.Decorations.cs b/src/Avalonia.Controls/TopLevelHost.Decorations.cs new file mode 100644 index 0000000000..f9cb20f511 --- /dev/null +++ b/src/Avalonia.Controls/TopLevelHost.Decorations.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Chrome; +using Avalonia.Input; +using Avalonia.Reactive; + +namespace Avalonia.Controls; + +internal partial class TopLevelHost +{ + + /// + /// Wrapper that holds a single visual child, used to host decoration layer content + /// extracted from the decorations template. + /// + private class LayerWrapper : Control + { + public Control? Inner + { + get => field; + set + { + if (field == value) + return; + if (field != null) + VisualChildren.Remove(field); + field = value; + if (field != null) + VisualChildren.Add(field); + } + } + } + + private readonly TopLevel _topLevel; + private WindowDrawnDecorations? _decorations; + private LayerWrapper? _underlay; + private LayerWrapper? _overlay; + private LayerWrapper? _fullscreenPopover; + private ResizeGripLayer? _resizeGrips; + private IDisposable? _decorationsSubscriptions; + + /// + /// Gets the current drawn decorations instance, if active. + /// + internal WindowDrawnDecorations? Decorations => _decorations; + + /// + /// Enables drawn window decorations with the specified parts. + /// Creates the decorations instance, applies the template, and inserts layers into the visual tree. + /// + internal void EnableDecorations(DrawnWindowDecorationParts parts) + { + if (_decorations != null) + { + // Layers persist across part changes; pseudo-classes driven by EnabledParts + // control visibility of individual decoration elements in the theme. + _decorations.EnabledParts = parts; + if (_resizeGrips != null) + _resizeGrips.IsVisible = parts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); + return; + } + + _decorations = new WindowDrawnDecorations(); + _decorations.EnabledParts = parts; + + // Set up logical parenting + LogicalChildren.Add(_decorations); + + // Create layer wrappers + _underlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeUnderlay" }; + _overlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeOverlay" }; + _fullscreenPopover = new LayerWrapper() + { + IsVisible = false, [AutomationProperties.AutomationIdProperty] = "PopoverWindowChrome" + }; + + // Insert layers: underlay below TopLevel, overlay and popover above + // Visual order: underlay(0), TopLevel(1), overlay(2), fullscreenPopover(3), resizeGrips(4) + VisualChildren.Insert(0, _underlay); + VisualChildren.Add(_overlay); + VisualChildren.Add(_fullscreenPopover); + + // Always create resize grips; visibility is controlled by EnabledParts + _resizeGrips = new ResizeGripLayer(); + _resizeGrips.IsVisible = parts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); + VisualChildren.Add(_resizeGrips); + + // Attach to window if available + if (_topLevel is Window window) + _decorations.Attach(window); + + // Subscribe to template changes to re-apply and geometry changes for resize grips + _decorations.EffectiveGeometryChanged += OnDecorationsGeometryChanged; + _decorationsSubscriptions = _decorations.GetObservable(WindowDrawnDecorations.TemplateProperty) + .Subscribe(_ => ApplyDecorationsTemplate()); + + ApplyDecorationsTemplate(); + InvalidateMeasure(); + _decorationsOverlayPeer?.InvalidateChildren(); + } + + /// + /// Disables drawn window decorations and removes all layers. + /// + internal void DisableDecorations() + { + if (_decorations == null) + return; + + _decorationsSubscriptions?.Dispose(); + _decorationsSubscriptions = null; + + _decorations.EffectiveGeometryChanged -= OnDecorationsGeometryChanged; + _decorations.Detach(); + + // Remove layers + if (_underlay != null) + { + VisualChildren.Remove(_underlay); + _underlay = null; + } + if (_overlay != null) + { + VisualChildren.Remove(_overlay); + _overlay = null; + } + if (_fullscreenPopover != null) + { + VisualChildren.Remove(_fullscreenPopover); + _fullscreenPopover = null; + } + if (_resizeGrips != null) + { + VisualChildren.Remove(_resizeGrips); + _resizeGrips = null; + } + + // Clean up logical tree + LogicalChildren.Remove(_decorations); + _decorations = null; + _decorationsOverlayPeer?.InvalidateChildren(); + } + + private void ApplyDecorationsTemplate() + { + if (_decorations == null) + return; + + _decorations.ApplyTemplate(); + + var content = _decorations.Content; + if (_underlay != null) + _underlay.Inner = content?.Underlay; + if (_overlay != null) + _overlay.Inner = content?.Overlay; + if (_fullscreenPopover != null) + _fullscreenPopover.Inner = content?.FullscreenPopover; + + UpdateResizeGripThickness(); + } + + private void UpdateResizeGripThickness() + { + if (_resizeGrips == null || _decorations == null) + return; + + var frame = _decorations.FrameThickness; + var shadow = _decorations.ShadowThickness; + // Grips strictly cover frame + shadow area, never client area + _resizeGrips.GripThickness = new Thickness( + frame.Left + shadow.Left, + frame.Top + shadow.Top, + frame.Right + shadow.Right, + frame.Bottom + shadow.Bottom); + } + + internal void UpdateResizeGrips() + { + UpdateResizeGripThickness(); + } + + private void OnDecorationsGeometryChanged() + { + UpdateResizeGripThickness(); + + // Notify Window to update margins + if (_topLevel is Window window) + window.OnDrawnDecorationsGeometryChanged(); + } + + /// + /// Shows or hides the fullscreen popover based on the window state. + /// Called by Window when window state changes. + /// + internal void SetFullscreenPopoverEnabled(bool enabled) + { + if (_fullscreenPopover == null) + return; + + if (enabled) + { + // In fullscreen mode, hide overlay and underlay, enable popover hover detection + if (_overlay != null) + _overlay.IsVisible = false; + if (_underlay != null) + _underlay.IsVisible = false; + // Popover starts hidden, will show on hover at top edge + _fullscreenPopover.IsVisible = false; + _fullscreenPopoverEnabled = true; + } + else + { + // Not fullscreen: show overlay and underlay, hide popover + if (_overlay != null) + _overlay.IsVisible = true; + if (_underlay != null) + _underlay.IsVisible = true; + _fullscreenPopover.IsVisible = false; + _fullscreenPopoverEnabled = false; + } + _decorationsOverlayPeer?.InvalidateChildren(); + } + + private bool _fullscreenPopoverEnabled; + private const double PopoverTriggerZoneHeight = 1; + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (_fullscreenPopoverEnabled && _fullscreenPopover != null) + { + var pos = e.GetPosition(this); + // Use DefaultTitleBarHeight since TitleBarHeight is 0 in fullscreen + var titleBarHeight = _decorations?.DefaultTitleBarHeight ?? 30; + + if (!_fullscreenPopover.IsVisible && pos.Y <= PopoverTriggerZoneHeight) + { + _fullscreenPopover.IsVisible = true; + _decorationsOverlayPeer?.InvalidateChildren(); + } + else if (pos.Y > titleBarHeight && _fullscreenPopover.IsVisible) + { + _fullscreenPopover.IsVisible = false; + _decorationsOverlayPeer?.InvalidateChildren(); + } + } + } +} diff --git a/src/Avalonia.Controls/TopLevelHost.Peers.cs b/src/Avalonia.Controls/TopLevelHost.Peers.cs new file mode 100644 index 0000000000..44d49a61fe --- /dev/null +++ b/src/Avalonia.Controls/TopLevelHost.Peers.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation; +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls; + +partial class TopLevelHost +{ + + protected override AutomationPeer OnCreateAutomationPeer() + => new TopLevelHostAutomationPeer(this); + + private DecorationsOverlaysAutomationPeer? _decorationsOverlayPeer; + + public AutomationPeer GetOrCreateDecorationsOverlaysPeer() => + _decorationsOverlayPeer ??= new DecorationsOverlaysAutomationPeer(this, _topLevel); + + /// + /// Automation peer that returns no children. The automation tree is managed + /// by WindowAutomationPeer, which directly includes decoration content. + /// Without this, EnsureConnected would walk up through TopLevelHost and + /// set Window's parent peer to TopLevelHost's peer, breaking the root. + /// + private class TopLevelHostAutomationPeer(TopLevelHost owner) : ControlAutomationPeer(owner) + { + protected override IReadOnlyList? GetChildrenCore() => null; + } + + private class DecorationsOverlaysAutomationPeer(TopLevelHost host, TopLevel topLevel) : AutomationPeer + { + private List _children = new(); + private bool _childrenValid = false; + protected override void BringIntoViewCore() => topLevel.GetOrCreateAutomationPeer().BringIntoView(); + + protected override string? GetAcceleratorKeyCore() => null; + + protected override string? GetAccessKeyCore() => null; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Group; + + protected override string? GetAutomationIdCore() => "AvaloniaWindowChrome"; + + protected override Rect GetBoundingRectangleCore() => host.Bounds; + + protected override IReadOnlyList GetOrCreateChildrenCore() + { + if (!_childrenValid) + { + var newChildren = (new LayerWrapper?[] { host._fullscreenPopover, host._overlay, host._underlay }) + .Where(c => c?.IsVisible == true) + .Select(c => c!.GetOrCreateAutomationPeer()).ToList(); + + foreach (var peer in _children.Except(newChildren)) + peer.TrySetParent(null); + foreach (var peer in newChildren) + peer.TrySetParent(this); + _children = newChildren; + _childrenValid = true; + } + + return _children; + + } + + public void InvalidateChildren() + { + if (_childrenValid) + { + _childrenValid = false; + RaiseChildrenChangedEvent(); + } + } + + protected override string GetClassNameCore() => "WindowChrome"; + + protected override AutomationPeer? GetLabeledByCore() => null; + + protected override string? GetNameCore() => "WindowChrome"; + + protected override AutomationPeer? GetParentCore() => topLevel.GetOrCreateAutomationPeer(); + + protected override bool HasKeyboardFocusCore() => _children?.Any(x => x.HasKeyboardFocus()) == true; + protected override bool IsKeyboardFocusableCore() => false; + + protected override bool IsContentElementCore() => false; + + protected override bool IsControlElementCore() => true; + + protected override bool IsEnabledCore() => true; + + protected override void SetFocusCore(){} + + protected override bool ShowContextMenuCore() => false; + + protected internal override bool TrySetParent(AutomationPeer? parent) => false; + } +} diff --git a/src/Avalonia.Controls/TopLevelHost.cs b/src/Avalonia.Controls/TopLevelHost.cs index 3c7a2f0a98..d2d3ddf8fa 100644 --- a/src/Avalonia.Controls/TopLevelHost.cs +++ b/src/Avalonia.Controls/TopLevelHost.cs @@ -1,13 +1,18 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Chrome; using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Controls; /// -/// For now this is a stub class that is needed to prevent people from assuming that TopLevel sits at the root of the -/// visual tree. -/// In future 12.x releases it will serve more roles like hosting popups and CSD. +/// Hosts the TopLevel and, when enabled, drawn decoration layers (underlay, overlay, fullscreen popover). +/// Serves as the visual root for PresentationSource. /// -internal class TopLevelHost : Control +internal partial class TopLevelHost : Control { static TopLevelHost() { @@ -16,6 +21,7 @@ internal class TopLevelHost : Control public TopLevelHost(TopLevel tl) { + _topLevel = tl; VisualChildren.Add(tl); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 222bc62f4f..16bd8c5f2a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Avalonia.Automation.Peers; +using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; @@ -108,9 +109,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); - public static readonly StyledProperty ExtendClientAreaChromeHintsProperty = - AvaloniaProperty.Register(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default); - public static readonly StyledProperty ExtendClientAreaTitleBarHeightHintProperty = AvaloniaProperty.Register(nameof(ExtendClientAreaTitleBarHeightHint), -1); @@ -224,6 +222,7 @@ namespace Avalonia.Controls static Window() { BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); + ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler((w, _) => w.OnTitleBarHeightHintChanged()); } /// @@ -257,7 +256,6 @@ namespace Avalonia.Controls CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state); CreatePlatformImplBinding(ExtendClientAreaToDecorationsHintProperty, hint => PlatformImpl!.SetExtendClientAreaToDecorationsHint(hint)); - CreatePlatformImplBinding(ExtendClientAreaChromeHintsProperty, hint => PlatformImpl!.SetExtendClientAreaChromeHints(hint)); CreatePlatformImplBinding(ExtendClientAreaTitleBarHeightHintProperty, height => PlatformImpl!.SetExtendClientAreaTitleBarHeightHint(height)); CreatePlatformImplBinding(MinWidthProperty, UpdateMinMaxSize); @@ -317,16 +315,6 @@ namespace Avalonia.Controls set => SetValue(ExtendClientAreaToDecorationsHintProperty, value); } - /// - /// Gets or Sets the that control - /// how the chrome looks when the client area is extended. - /// - public ExtendClientAreaChromeHints ExtendClientAreaChromeHints - { - get => GetValue(ExtendClientAreaChromeHintsProperty); - set => SetValue(ExtendClientAreaChromeHintsProperty, value); - } - /// /// Gets or Sets the TitlebarHeightHint for when the client area is extended. /// A value of -1 will cause the titlebar to be auto sized to the OS default. @@ -630,13 +618,138 @@ namespace Avalonia.Controls { StartRendering(); } + + // Update fullscreen popover visibility + TopLevelHost.SetFullscreenPopoverEnabled(state == WindowState.FullScreen); + + // Update decoration parts for the new window state + UpdateDrawnDecorationParts(); } protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended) { IsExtendedIntoWindowDecorations = isExtended; - WindowDecorationMargin = PlatformImpl?.ExtendedMargins ?? default; OffScreenMargin = PlatformImpl?.OffScreenMargin ?? default; + + UpdateDrawnDecorations(); + } + + private void UpdateDrawnDecorations() + { + var needsDrawnDecorations = PlatformImpl?.NeedsManagedDecorations ?? false; + + var parts = needsDrawnDecorations ? ComputeDecorationParts() : DrawnWindowDecorationParts.None; + if (parts != DrawnWindowDecorationParts.None) + { + TopLevelHost.EnableDecorations(parts); + + // Forward ExtendClientAreaTitleBarHeightHint to decoration TitleBarHeight + var decorations = TopLevelHost.Decorations; + if (decorations != null) + { + var hint = ExtendClientAreaTitleBarHeightHint; + if (hint >= 0) + decorations.TitleBarHeightOverride = hint; + } + } + else + { + TopLevelHost.DisableDecorations(); + } + + UpdateDrawnDecorationMargins(); + } + + /// + /// Updates decoration parts based on current window state without + /// re-creating the decorations instance. + /// + private void UpdateDrawnDecorationParts() + { + if (TopLevelHost.Decorations == null) + return; + + TopLevelHost.EnableDecorations(ComputeDecorationParts()); + } + + private Chrome.DrawnWindowDecorationParts ComputeDecorationParts() + { + var platformNeeds = PlatformImpl?.RequestedDrawnDecorations ?? PlatformRequestedDrawnDecoration.None; + var parts = Chrome.DrawnWindowDecorationParts.None; + if (SystemDecorations != SystemDecorations.None) + { + if (platformNeeds.HasFlag(PlatformRequestedDrawnDecoration.TitleBar) && + SystemDecorations == SystemDecorations.Full) + parts |= Chrome.DrawnWindowDecorationParts.TitleBar; + if (platformNeeds.HasFlag(PlatformRequestedDrawnDecoration.Shadow)) + parts |= Chrome.DrawnWindowDecorationParts.Shadow; + if (platformNeeds.HasFlag(PlatformRequestedDrawnDecoration.Border)) + parts |= Chrome.DrawnWindowDecorationParts.Border; + if (platformNeeds.HasFlag(PlatformRequestedDrawnDecoration.ResizeGrips) && CanResize) + parts |= Chrome.DrawnWindowDecorationParts.ResizeGrips; + + + // In fullscreen: no shadow, border, resize grips, or titlebar (popover takes over) + if (WindowState == WindowState.FullScreen) + { + parts &= ~(Chrome.DrawnWindowDecorationParts.Shadow + | Chrome.DrawnWindowDecorationParts.Border + | Chrome.DrawnWindowDecorationParts.ResizeGrips + | Chrome.DrawnWindowDecorationParts.TitleBar); + } + // In maximized: no shadow, border, or resize grips (titlebar stays) + else if (WindowState == WindowState.Maximized) + { + parts &= ~(Chrome.DrawnWindowDecorationParts.Shadow + | Chrome.DrawnWindowDecorationParts.Border + | Chrome.DrawnWindowDecorationParts.ResizeGrips); + } + } + + return parts; + } + + private void UpdateDrawnDecorationMargins() + { + var decorations = TopLevelHost.Decorations; + if (decorations == null) + { + WindowDecorationMargin = PlatformImpl?.ExtendedMargins ?? default; + return; + } + + var parts = decorations.EnabledParts; + var titleBarHeight = parts.HasFlag(Chrome.DrawnWindowDecorationParts.TitleBar) + ? decorations.TitleBarHeight : 0; + var frame = parts.HasFlag(Chrome.DrawnWindowDecorationParts.Border) + ? decorations.FrameThickness : default; + var shadow = parts.HasFlag(Chrome.DrawnWindowDecorationParts.Shadow) + ? decorations.ShadowThickness : default; + WindowDecorationMargin = new Thickness( + frame.Left + shadow.Left, + titleBarHeight + frame.Top + shadow.Top, + frame.Right + shadow.Right, + frame.Bottom + shadow.Bottom); + } + + private void OnTitleBarHeightHintChanged() + { + var decorations = TopLevelHost.Decorations; + if (decorations == null) + return; + + decorations.TitleBarHeightOverride = ExtendClientAreaTitleBarHeightHint; + + UpdateDrawnDecorationMargins(); + } + + /// + /// Called by TopLevelHost when decoration effective geometry changes + /// (e.g. theme changes Default* values, or EnabledParts changes). + /// + internal void OnDrawnDecorationsGeometryChanged() + { + UpdateDrawnDecorationMargins(); } /// @@ -771,6 +884,10 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + + // Enable drawn decorations before layout so margins are computed + UpdateDrawnDecorations(); + _shown = true; IsVisible = true; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 8557ffcb30..4fbe77d485 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -92,6 +92,7 @@ namespace Avalonia.DesignerSupport.Remote public Action ExtendClientAreaToDecorationsChanged { get; set; } + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations { get; } public Thickness ExtendedMargins { get; } = new Thickness(); public bool IsClientAreaExtendedToDecorations { get; } @@ -163,10 +164,6 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { - } - public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index c50ea1e777..c9b6515ff6 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Platform; @@ -47,6 +48,9 @@ namespace Avalonia.DesignerSupport.Remote public Action? ExtendClientAreaToDecorationsChanged { get; set; } + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations => IsClientAreaExtendedToDecorations + ? PlatformRequestedDrawnDecoration.TitleBar + : default; public Thickness ExtendedMargins { get; } = new Thickness(); public Thickness OffScreenMargin { get; } = new Thickness(); @@ -174,10 +178,6 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { - } - public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index b9539c3fb7..7adbfca6af 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -20,6 +20,7 @@ namespace Avalonia.Native private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; private bool _canResize = true; private bool _canMaximize = true; + private Controls.SystemDecorations _decorations = Controls.SystemDecorations.Full; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(factory) { @@ -92,7 +93,9 @@ namespace Avalonia.Native public void SetSystemDecorations(Controls.SystemDecorations enabled) { + _decorations = enabled; _native.SetDecorations((Interop.SystemDecorations)enabled); + InvalidateExtendedMargins(); } public void SetTitleBarColor(Avalonia.Media.Color color) @@ -115,6 +118,8 @@ namespace Avalonia.Native public Action? ExtendClientAreaToDecorationsChanged { get; set; } + // Extension is handled by native backend + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations => default; public Thickness ExtendedMargins { get; private set; } public Thickness OffScreenMargin { get; } = new Thickness(); @@ -181,13 +186,13 @@ namespace Avalonia.Native if(_native is MicroComProxyBase pb && pb.IsDisposed) return; - if (WindowState == WindowState.FullScreen) + if (WindowState == WindowState.FullScreen || !_isExtended || _decorations != Controls.SystemDecorations.Full) { ExtendedMargins = new Thickness(); } else { - ExtendedMargins = _isExtended ? new Thickness(0, _extendTitleBarHeight == -1 ? _native.ExtendTitleBarHeight : _extendTitleBarHeight, 0, 0) : new Thickness(); + ExtendedMargins = new Thickness(0, _extendTitleBarHeight == -1 ? _native.ExtendTitleBarHeight : _extendTitleBarHeight, 0, 0); } ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended); @@ -203,21 +208,13 @@ namespace Avalonia.Native InvalidateExtendedMargins(); } - /// - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { - _native.SetExtendClientAreaHints ((AvnExtendClientAreaChromeHints)hints); - } - /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { _extendTitleBarHeight = titleBarHeight; _native.SetExtendTitleBarHeight(titleBarHeight); - ExtendedMargins = _isExtended ? new Thickness(0, titleBarHeight == -1 ? _native.ExtendTitleBarHeight : titleBarHeight, 0, 0) : new Thickness(); - - ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended); + InvalidateExtendedMargins(); } /// diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 6a3781ed89..0ab5189ffd 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -591,15 +591,6 @@ enum AvnMenuItemToggleType Radio } -enum AvnExtendClientAreaChromeHints -{ - AvnNoChrome = 0, - AvnSystemChrome = 0x01, - AvnPreferSystemChrome = 0x02, - AvnOSXThickTitleBar = 0x08, - AvnDefaultChrome = AvnPreferSystemChrome, -} - enum AvnPlatformResizeReason { ResizeUnspecified, @@ -810,7 +801,6 @@ interface IAvnWindow : IAvnWindowBase HRESULT GetWindowState(AvnWindowState*ret); HRESULT TakeFocusFromChildren(); HRESULT SetExtendClientArea(bool enable); - HRESULT SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints); HRESULT GetExtendTitleBarHeight(double*ret); HRESULT SetExtendTitleBarHeight(double value); HRESULT GetWindowZOrder(long*ret); diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml index 95b77e1d73..2613a0a133 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -821,7 +821,7 @@ - + @@ -1648,7 +1648,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml deleted file mode 100644 index d9f9343925..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ /dev/null @@ -1,119 +0,0 @@ - - - 45 - 30 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 18996d6a2d..415a1ea982 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -18,7 +18,6 @@ - @@ -52,12 +51,12 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml deleted file mode 100644 index 180de6f0fc..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml new file mode 100644 index 0000000000..d00007010c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml @@ -0,0 +1,215 @@ + + + 45 + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 766e034ee5..35f8abf64b 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -42,7 +42,7 @@ - + @@ -87,7 +87,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml deleted file mode 100644 index 98477eea5c..0000000000 --- a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - 45 - 30 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 51171051d1..fe2139e771 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -22,8 +22,7 @@ - - + diff --git a/src/Avalonia.Themes.Simple/Controls/TitleBar.xaml b/src/Avalonia.Themes.Simple/Controls/TitleBar.xaml deleted file mode 100644 index ce0ac90169..0000000000 --- a/src/Avalonia.Themes.Simple/Controls/TitleBar.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml b/src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml new file mode 100644 index 0000000000..a7ae9a7f2e --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml @@ -0,0 +1,221 @@ + + + 45 + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec8a915ea0..bff986d2d1 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Avalonia.Controls.Platform; @@ -469,7 +470,26 @@ namespace Avalonia /// Use this if you need to use GLib-based libraries on the main thread /// public bool UseGLibMainLoop { get; set; } - + + /// + /// Enables client-side drawn window decorations on X11. + /// When true and ExtendClientAreaToDecorationsHint is set on a window, + /// Avalonia will draw its own decorations (titlebar, borders, resize grips) + /// instead of using the X11 window manager decorations. + /// + [Experimental("AVALONIA_X11_CSD" + #if NET10_0_OR_GREATER + , Message = "Experimental, used mostly for testing" + #endif + )] + public bool? EnableDrawnDecorations + { + get => EnableDrawnDecorationsInternal; + set => EnableDrawnDecorationsInternal = value; + } + + internal bool? EnableDrawnDecorationsInternal { get; set; } + /// /// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame /// and rethrowing an exception. However, if there is no Avalonia-controlled run loop frame, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 11dff2f2a5..01190709b6 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -508,7 +508,7 @@ namespace Avalonia.X11 public Thickness OffScreenMargin { get; } = new Thickness(); - public bool IsClientAreaExtendedToDecorations { get; } + public bool IsClientAreaExtendedToDecorations { get; private set; } public Action? Closed { get; set; } public Action? PositionChanged { get; set; } @@ -865,6 +865,7 @@ namespace Avalonia.X11 return rv; } + private SystemDecorations _requestedSystemDecorations = SystemDecorations.Full; private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private bool _canMinimize = true; @@ -921,7 +922,44 @@ namespace Avalonia.X11 private void ScheduleInput(RawInputEventArgs args) { if (args is RawPointerEventArgs mouse) + { mouse.Position = mouse.Position / RenderScaling; + + // Chrome hit-test for drawn decorations + if (_extendClientAreaToDecorations + && mouse.Type == RawPointerEventType.LeftButtonDown + && _inputRoot is { } inputRoot) + { + var chromeRole = inputRoot.HitTestChromeElement(mouse.Position); + if (chromeRole is { } role) + { + var moveResizeSide = role switch + { + WindowDecorationsElementRole.TitleBar => NetWmMoveResize._NET_WM_MOVERESIZE_MOVE, + WindowDecorationsElementRole.ResizeN when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOP, + WindowDecorationsElementRole.ResizeS when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOM, + WindowDecorationsElementRole.ResizeE when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_RIGHT, + WindowDecorationsElementRole.ResizeW when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_LEFT, + WindowDecorationsElementRole.ResizeNE when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOPRIGHT, + WindowDecorationsElementRole.ResizeNW when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOPLEFT, + WindowDecorationsElementRole.ResizeSE when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT, + WindowDecorationsElementRole.ResizeSW when _canResize => NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT, + _ => (NetWmMoveResize?)null + }; + + if (moveResizeSide.HasValue) + { + var pos = GetCursorPos(_x11); + XUngrabPointer(_x11.Display, IntPtr.Zero); + SendNetWMMessage(_x11.Atoms._NET_WM_MOVERESIZE, + (IntPtr)pos.x, (IntPtr)pos.y, + (IntPtr)moveResizeSide.Value, + (IntPtr)1, (IntPtr)1); + return; + } + } + } + } if (args is RawDragEvent drag) drag.Location = drag.Location / RenderScaling; @@ -1143,7 +1181,23 @@ namespace Avalonia.X11 public void SetSystemDecorations(SystemDecorations enabled) { - _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None; + _requestedSystemDecorations = enabled; + UpdateEffectiveSystemDecorations(); + } + + private void UpdateEffectiveSystemDecorations() + { + // When extending client area, always hide WM decorations (we draw our own) + var effective = _extendClientAreaToDecorations + ? SystemDecorations.None + : (_requestedSystemDecorations == SystemDecorations.Full + ? SystemDecorations.Full + : SystemDecorations.None); + + if (_systemDecorations == effective) + return; + + _systemDecorations = effective; UpdateMotifHints(); UpdateSizeHints(null); } @@ -1455,12 +1509,21 @@ namespace Avalonia.X11 } } + private bool _extendClientAreaToDecorations; + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) { - } + if (_platform.Options.EnableDrawnDecorationsInternal != true) + return; - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { + if (_extendClientAreaToDecorations == extendIntoClientAreaHint) + return; + + _extendClientAreaToDecorations = extendIntoClientAreaHint; + UpdateEffectiveSystemDecorations(); + + IsClientAreaExtendedToDecorations = extendIntoClientAreaHint; + ExtendClientAreaToDecorationsChanged?.Invoke(extendIntoClientAreaHint); } public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) @@ -1538,7 +1601,15 @@ namespace Avalonia.X11 public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); - public bool NeedsManagedDecorations => false; + public bool NeedsManagedDecorations => _extendClientAreaToDecorations; + + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations => + _extendClientAreaToDecorations + ? PlatformRequestedDrawnDecoration.Border + | PlatformRequestedDrawnDecoration.ResizeGrips + | PlatformRequestedDrawnDecoration.TitleBar + | PlatformRequestedDrawnDecoration.Shadow + : PlatformRequestedDrawnDecoration.None; public bool IsEnabled => !_disabled && !_mode.BlockInput; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 8d88cebc13..6ed907aa77 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; @@ -253,6 +254,7 @@ namespace Avalonia.Headless public Action? ExtendClientAreaToDecorationsChanged { get; set; } public bool NeedsManagedDecorations => false; + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations { get; } public Thickness ExtendedMargins => new Thickness(); @@ -416,11 +418,6 @@ namespace Avalonia.Headless } - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { - - } - public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6dd2690414..3154d094a5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/WindowDrawnDecorationsTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/WindowDrawnDecorationsTemplate.cs new file mode 100644 index 0000000000..f4fea5e01f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/WindowDrawnDecorationsTemplate.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls.Chrome; +using Avalonia.Controls.Templates; +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Markup.Xaml.Templates; + +[ControlTemplateScope] +public class WindowDrawnDecorationsTemplate : IWindowDrawnDecorationsTemplate, ITemplate +{ + [Content] + [TemplateContent(TemplateResultType = typeof(WindowDrawnDecorationsContent))] + public object? Content { get; set; } + + public TemplateResult Build() => + TemplateContent.Load(Content) + ?? throw new InvalidOperationException(); + + object? ITemplate.Build() => Build().Result; +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index 12a5c34253..756ab5775d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -224,6 +224,38 @@ namespace Avalonia.Win32 private HitTestValues HitTestVisual(IntPtr lParam) { var position = PointToClient(PointFromLParam(lParam)); + + // First, check new cross-platform ElementRole via chrome hit-test + if (_owner is IInputRoot inputRoot) + { + var chromeRole = inputRoot.HitTestChromeElement(position); + if (chromeRole.HasValue) + { + return chromeRole.Value switch + { + // DecorationsElement/User = interactive chrome element (e.g., caption button) + // — signal to redirect NC input to client input. + WindowDecorationsElementRole.DecorationsElement => HitTestValues.HTCLIENT, + WindowDecorationsElementRole.User => HitTestValues.HTCLIENT, + WindowDecorationsElementRole.TitleBar => HitTestValues.HTCAPTION, + WindowDecorationsElementRole.ResizeN => HitTestValues.HTTOP, + WindowDecorationsElementRole.ResizeS => HitTestValues.HTBOTTOM, + WindowDecorationsElementRole.ResizeE => HitTestValues.HTRIGHT, + WindowDecorationsElementRole.ResizeW => HitTestValues.HTLEFT, + WindowDecorationsElementRole.ResizeNE => HitTestValues.HTTOPRIGHT, + WindowDecorationsElementRole.ResizeNW => HitTestValues.HTTOPLEFT, + WindowDecorationsElementRole.ResizeSE => HitTestValues.HTBOTTOMRIGHT, + WindowDecorationsElementRole.ResizeSW => HitTestValues.HTBOTTOMLEFT, + WindowDecorationsElementRole.CloseButton => HitTestValues.HTCLOSE, + WindowDecorationsElementRole.MinimizeButton => HitTestValues.HTMINBUTTON, + WindowDecorationsElementRole.MaximizeButton => HitTestValues.HTMAXBUTTON, + WindowDecorationsElementRole.FullScreenButton => HitTestValues.HTCLIENT, + _ => HitTestValues.HTNOWHERE + }; + } + } + + // Fall back to Win32-specific NonClientHitTestResult attached property if (_owner?.RootElement is {} window) { var visual = window.GetVisualAt(position, x => diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 081c488cd4..57ba62307a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -100,7 +100,6 @@ namespace Avalonia.Win32 private Size _maxSize; private POINT _maxTrackSize; private WindowImpl? _parent; - private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; private bool _hiddenWindowIsParent; @@ -1150,8 +1149,7 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - if (_extendChromeHints.HasAnyFlag(ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.PreferSystemChrome) && - _windowProperties.Decorations == SystemDecorations.Full) + if (_windowProperties.Decorations == SystemDecorations.Full) { if (_extendTitleBarHint != -1) borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); @@ -1167,7 +1165,7 @@ namespace Avalonia.Win32 margins.cxRightWidth = defaultMargin; margins.cyBottomHeight = defaultMargin; - margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : defaultMargin; + margins.cyTopHeight = defaultMargin; if (WindowState == WindowState.Maximized) { @@ -1232,8 +1230,7 @@ namespace Avalonia.Win32 } } - if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && - !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome))) + if (!_isClientAreaExtended) { EnableCloseButton(_hwnd); } @@ -1598,13 +1595,6 @@ namespace Avalonia.Win32 ExtendClientArea(); } - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) - { - _extendChromeHints = hints; - - ExtendClientArea(); - } - /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { @@ -1620,7 +1610,12 @@ namespace Avalonia.Win32 public Action? ExtendClientAreaToDecorationsChanged { get; set; } /// - public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); + public bool NeedsManagedDecorations => _isClientAreaExtended; + + public PlatformRequestedDrawnDecoration RequestedDrawnDecorations => + _isClientAreaExtended + ? PlatformRequestedDrawnDecoration.TitleBar + : PlatformRequestedDrawnDecoration.None; /// public Thickness ExtendedMargins => _extendedMargins; @@ -1731,3 +1726,4 @@ namespace Avalonia.Win32 } } } + diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 43269f3919..560e2727f9 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -62,11 +62,13 @@ namespace Avalonia.IntegrationTests.Appium public static WindowChrome GetClientChromeButtons(this AppiumWebElement window) { - var titlebar = window.FindElementsByAccessibilityId("AvaloniaTitleBar")?.FirstOrDefault(); - var closeButton = titlebar?.FindElementByName("Close"); - var minimizeButton = titlebar?.FindElementByName("Minimize"); - var maximizeButton = titlebar?.FindElementByName("Maximize"); - return new(closeButton, minimizeButton, maximizeButton, null, titlebar); + var chrome = window.FindElementsByAccessibilityId("AvaloniaWindowChrome")?.FirstOrDefault(); + var titlebar = chrome?.FindElementsByAccessibilityId("AvaloniaTitleBar")?.FirstOrDefault(); + var closeButton = chrome?.FindElementsByAccessibilityId("Close")?.FirstOrDefault(); + var minimizeButton = chrome?.FindElementsByAccessibilityId("Minimize")?.FirstOrDefault(); + var maximizeButton = chrome?.FindElementsByAccessibilityId("Maximize")?.FirstOrDefault(); + var fullscreenButton = chrome?.FindElementsByAccessibilityId("Fullscreen")?.FirstOrDefault(); + return new(closeButton, minimizeButton, maximizeButton, fullscreenButton, titlebar); } public static string GetComboBoxValue(this AppiumWebElement element) diff --git a/tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs index 7b5d760d61..c792788ff1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs @@ -7,102 +7,22 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium; [Collection("Default")] -public class PointerTests_MacOS : TestBase, IDisposable +public class PointerTests_MacOS : TestBase { public PointerTests_MacOS(DefaultAppFixture fixture) : base(fixture, "Window Decorations") { } - - [PlatformFact(TestPlatforms.MacOS)] - public void OSXThickTitleBar_Pointer_Events_Continue_Outside_Window_During_Drag() - { - // issue #15696 - SetParameters(true, false, true, true, true); - - var showNewWindowDecorations = Session.FindElementByAccessibilityId("ShowNewWindowDecorations"); - showNewWindowDecorations.Click(); - - Thread.Sleep(1000); - - var secondaryWindow = Session.GetWindowById("SecondaryWindow"); - - var titleAreaControl = secondaryWindow.FindElementByAccessibilityId("TitleAreaControl"); - Assert.NotNull(titleAreaControl); - - new Actions(Session).MoveToElement(secondaryWindow).Perform(); - new Actions(Session).MoveToElement(titleAreaControl).Perform(); - new Actions(Session).DragAndDropToOffset(titleAreaControl, 50, -100).Perform(); - - var finalMoveCount = GetMoveCount(secondaryWindow); - var finalReleaseCount = GetReleaseCount(secondaryWindow); - - Assert.True(finalMoveCount >= 10, $"Expected at least 10 new mouse move events outside window, got {finalMoveCount})"); - Assert.Equal(1, finalReleaseCount); - - secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click(); - } - - [PlatformFact(TestPlatforms.MacOS)] - public void OSXThickTitleBar_Single_Click_Does_Not_Generate_DoubleTapped_Event() - { - SetParameters(true, false, true, true, true); - - var showNewWindowDecorations = Session.FindElementByAccessibilityId("ShowNewWindowDecorations"); - showNewWindowDecorations.Click(); - - Thread.Sleep(1000); - - var secondaryWindow = Session.GetWindowById("SecondaryWindow"); - var titleAreaControl = secondaryWindow.FindElementByAccessibilityId("TitleAreaControl"); - Assert.NotNull(titleAreaControl); - - // Verify initial state - counters should be 0 - var initialDoubleClickCount = GetDoubleClickCount(secondaryWindow); - var initialReleaseCount = GetReleaseCount(secondaryWindow); - var initialMouseDownCount = GetMouseDownCount(secondaryWindow); - Assert.Equal(0, initialDoubleClickCount); - Assert.Equal(0, initialReleaseCount); - Assert.Equal(0, initialMouseDownCount); - - // Perform a single click in titlebar area - secondaryWindow.MovePointerOver(); - titleAreaControl.MovePointerOver(); - titleAreaControl.SendClick(); - Thread.Sleep(800); - - // After first single click - mouse down = 1, release = 1, double-click = 0 - var afterFirstClickMouseDownCount = GetMouseDownCount(secondaryWindow); - var afterFirstClickReleaseCount = GetReleaseCount(secondaryWindow); - var afterFirstClickDoubleClickCount = GetDoubleClickCount(secondaryWindow); - Assert.Equal(1, afterFirstClickMouseDownCount); - Assert.Equal(1, afterFirstClickReleaseCount); - Assert.Equal(0, afterFirstClickDoubleClickCount); - - secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click(); - } private void SetParameters( bool extendClientArea, - bool forceSystemChrome, - bool preferSystemChrome, - bool macOsThickSystemChrome, bool showTitleAreaControl) { var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("WindowExtendClientAreaToDecorationsHint"); - var forceSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowForceSystemChrome"); - var preferSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowPreferSystemChrome"); - var macOsThickSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowMacThickSystemChrome"); var showTitleAreaControlCheckBox = Session.FindElementByAccessibilityId("WindowShowTitleAreaControl"); if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea) extendClientAreaCheckBox.Click(); - if (forceSystemChromeCheckBox.GetIsChecked() != forceSystemChrome) - forceSystemChromeCheckBox.Click(); - if (preferSystemChromeCheckBox.GetIsChecked() != preferSystemChrome) - preferSystemChromeCheckBox.Click(); - if (macOsThickSystemChromeCheckBox.GetIsChecked() != macOsThickSystemChrome) - macOsThickSystemChromeCheckBox.Click(); if (showTitleAreaControlCheckBox.GetIsChecked() != showTitleAreaControl) showTitleAreaControlCheckBox.Click(); } @@ -131,10 +51,11 @@ public class PointerTests_MacOS : TestBase, IDisposable return int.Parse(doubleClickCountTextBox.Text ?? "0"); } - public void Dispose() + public override void Dispose() { - SetParameters(false, false, false, false, false); + SetParameters(false, false); var applyButton = Session.FindElementByAccessibilityId("ApplyWindowDecorations"); applyButton.Click(); + base.Dispose(); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/TestBase.cs b/tests/Avalonia.IntegrationTests.Appium/TestBase.cs index 6378d05560..7e71a0a9ed 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestBase.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TestBase.cs @@ -1,9 +1,11 @@ -using OpenQA.Selenium; +using System; +using OpenQA.Selenium; using System.Threading; +using Xunit; namespace Avalonia.IntegrationTests.Appium; -public class TestBase +public class TestBase : IDisposable { protected TestBase(DefaultAppFixture fixture, string pageName) { @@ -35,4 +37,23 @@ public class TestBase } protected AppiumDriver Session { get; } + public virtual void Dispose() + { + #if DETECT_MISBEHAVING_TEST + for(var tries = 0; tries < 3; tries++) + { + try + { + Assert.NotNull(Session.FindElementByAccessibilityId("Pager")); + return; + } + catch + { + Thread.Sleep(3000); + } + } + throw new Exception( + "===== THE TEST HAS LEFT THE SESSION IN A BROKEN STATE. THE SUBSEQUENT TESTS WILL ALL FAIL ======="); + #endif + } } diff --git a/tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs b/tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs index 46f41796c5..8a6556e7b2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium; [Collection("Default")] -public class TrayIconTests : TestBase, IDisposable +public class TrayIconTests : TestBase { private readonly AppiumDriver? _rootSession; private const string TrayIconName = "IntegrationTestApp TrayIcon"; @@ -139,8 +139,9 @@ public class TrayIconTests : TestBase, IDisposable } } - public void Dispose() + public override void Dispose() { _rootSession?.Dispose(); + base.Dispose(); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowDecorationsTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowDecorationsTests.cs index 23045e612c..7f3168671a 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowDecorationsTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowDecorationsTests.cs @@ -5,7 +5,7 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium; [Collection("WindowDecorations")] -public class WindowDecorationsTests : TestBase, IDisposable +public class WindowDecorationsTests : TestBase { public WindowDecorationsTests(DefaultAppFixture fixture) : base(fixture, "Window Decorations") @@ -19,21 +19,14 @@ public class WindowDecorationsTests : TestBase, IDisposable var original = window.Size; // Step 1: keep extend client area to false, but adjust some value that should not have any effect. - SetParameters(false, false, false, false, 10); + SetParameters(false, 10); ApplyToCurrentWindow(); Assert.Equal(original, window.Size); - // Step 2: enable and disable extended system chrome. - SetParameters(true, true, false, false, 20); + // Step 2: enable and disable extended client area. + SetParameters(true, 20); ApplyToCurrentWindow(); - SetParameters(false, false, false, false, 20); - ApplyToCurrentWindow(); - Assert.Equal(original, window.Size); - - // Step 3: enable and disable extended client chrome. - SetParameters(true, false, true, false, 30); - ApplyToCurrentWindow(); - SetParameters(false, false, true, false, 20); + SetParameters(false, 20); ApplyToCurrentWindow(); Assert.Equal(original, window.Size); } @@ -41,10 +34,10 @@ public class WindowDecorationsTests : TestBase, IDisposable [Fact] public void Can_Restore_To_Non_Extended_State() { - SetParameters(true, true, false, false, 20); + SetParameters(true, 20); ApplyToCurrentWindow(); - SetParameters(false, false, false, false, 1000); + SetParameters(false, 1000); ApplyToCurrentWindow(); var currentWindow = Session.GetCurrentSingleWindow(); @@ -63,7 +56,7 @@ public class WindowDecorationsTests : TestBase, IDisposable [InlineData(50)] public void Should_Apply_Client_Chrome(int titleBarHeight) { - SetParameters(true, false, true, false, titleBarHeight); + SetParameters(true, titleBarHeight); ApplyToCurrentWindow(); @@ -82,13 +75,13 @@ public class WindowDecorationsTests : TestBase, IDisposable } } - [Theory] + [PlatformTheory(TestPlatforms.MacOS)] [InlineData(-1)] [InlineData(25)] [InlineData(50)] public void Should_Apply_System_Chrome(int titleBarHeight) { - SetParameters(true, true, false, false, titleBarHeight); + SetParameters(true, titleBarHeight); ApplyToCurrentWindow(); @@ -113,7 +106,7 @@ public class WindowDecorationsTests : TestBase, IDisposable [InlineData(50)] public void Should_Apply_Client_Chrome_On_New_Window(int titleBarHeight) { - SetParameters(true, false, true, false, titleBarHeight); + SetParameters(true, titleBarHeight); using (ApplyOnNewWindow()) { @@ -127,13 +120,13 @@ public class WindowDecorationsTests : TestBase, IDisposable } } - [PlatformTheory(TestPlatforms.MacOS)] // fix me, for some reason Windows doesn't return TitleBar system chrome for a child window. + [PlatformTheory(TestPlatforms.MacOS)] [InlineData(-1)] [InlineData(25)] [InlineData(50)] public void Should_Apply_System_Chrome_On_New_Window(int titleBarHeight) { - SetParameters(true, true, false, false, titleBarHeight); + SetParameters(true, titleBarHeight); using (ApplyOnNewWindow()) { @@ -191,25 +184,13 @@ public class WindowDecorationsTests : TestBase, IDisposable private void SetParameters( bool extendClientArea, - bool forceSystemChrome, - bool preferSystemChrome, - bool macOsThickSystemChrome, int titleBarHeight) { var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("WindowExtendClientAreaToDecorationsHint"); - var forceSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowForceSystemChrome"); - var preferSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowPreferSystemChrome"); - var macOsThickSystemChromeCheckBox = Session.FindElementByAccessibilityId("WindowMacThickSystemChrome"); var titleBarHeightBox = Session.FindElementByAccessibilityId("WindowTitleBarHeightHint"); if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea) extendClientAreaCheckBox.Click(); - if (forceSystemChromeCheckBox.GetIsChecked() != forceSystemChrome) - forceSystemChromeCheckBox.Click(); - if (preferSystemChromeCheckBox.GetIsChecked() != preferSystemChrome) - preferSystemChromeCheckBox.Click(); - if (macOsThickSystemChromeCheckBox.GetIsChecked() != macOsThickSystemChrome) - macOsThickSystemChromeCheckBox.Click(); titleBarHeightBox.Click(); titleBarHeightBox.Clear(); @@ -229,9 +210,10 @@ public class WindowDecorationsTests : TestBase, IDisposable return showNewWindowDecorations.OpenWindowWithClick(); } - public void Dispose() + public override void Dispose() { - SetParameters(false, false, false, false, -1); + SetParameters(false, -1); ApplyToCurrentWindow(); + base.Dispose(); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 7507711447..e1d5075c7e 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -78,11 +78,13 @@ namespace Avalonia.IntegrationTests.Appium } finally { - Session.FindElementByAccessibilityId("ExitFullscreen").Click(); + Session.FindElementByAccessibilityId("IntegrationTestApp_ExitFullscreen").Click(); } } +#if APPIUM2 [PlatformFact(TestPlatforms.MacOS)] +#endif public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() { var mainWindow = Session.FindElementByAccessibilityId("MainWindow"); @@ -95,7 +97,9 @@ namespace Avalonia.IntegrationTests.Appium } } +#if APPIUM2 [PlatformFact(TestPlatforms.MacOS)] +#endif public void WindowOrder_Owned_Dialog_Stays_InFront_Of_FullScreen_Parent() { var mainWindow = Session.FindElementByAccessibilityId("MainWindow"); @@ -119,7 +123,7 @@ namespace Avalonia.IntegrationTests.Appium } // Exit fullscreen by menu shortcut Command+R - mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + mainWindow.FindElementByAccessibilityId("IntegrationTestApp_ExitFullscreen").Click(); // Wait for restore transition. Thread.Sleep(1000); @@ -168,7 +172,7 @@ namespace Avalonia.IntegrationTests.Appium // Failed here due to #9565: main window is no longer visible as the main space is now shown instead // of the fullscreen space. - mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + mainWindow.FindElementByAccessibilityId("IntegrationTestApp_ExitFullscreen").Click(); // Wait for restore transition. Thread.Sleep(1000); diff --git a/tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs b/tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs index 9baa30df7e..cae559e605 100644 --- a/tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs @@ -1,11 +1,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using Avalonia.Automation; using Avalonia.Controls; -using Avalonia.Controls.Chrome; using Avalonia.Interactivity; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.VisualTree; using Xunit; @@ -42,7 +41,6 @@ public abstract class ExtendClientAreaWindowTests : IDisposable WindowState = state, SystemDecorations = Decorations, ExtendClientAreaToDecorationsHint = true, - ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome, Width = ClientWidth, Height = ClientHeight, WindowStartupLocation = WindowStartupLocation.Manual, @@ -139,13 +137,15 @@ public abstract class ExtendClientAreaWindowTests : IDisposable protected (double TitleBarHeight, double ButtonsHeight) GetTitleBarInfo() { - var titleBar = Window.GetVisualDescendants().OfType().FirstOrDefault(); - Assert.NotNull(titleBar); - - var buttons = titleBar.GetVisualDescendants().OfType().FirstOrDefault(); - Assert.NotNull(buttons); - - return (titleBar.Height, buttons.Height); + var host = Window.GetVisualParent()!; + host.GetLayoutManager()!.ExecuteLayoutPass(); + + var titlebar = host.GetVisualDescendants().FirstOrDefault(c => AutomationProperties.GetAutomationId(c) == "AvaloniaTitleBar"); + var closeButton = host.GetVisualDescendants().FirstOrDefault(c => AutomationProperties.GetAutomationId(c) == "Close"); + + return ( + titlebar?.IsEffectivelyVisible == true ? titlebar.Bounds.Height : 0, + closeButton?.IsEffectivelyVisible == true ? closeButton.Bounds.Height : 0); } private void AssertNoTitleBar() @@ -188,23 +188,11 @@ public abstract class ExtendClientAreaWindowTests : IDisposable protected override void VerifyNormalState(bool canResize) { AssertHasBorder(); - - if (canResize) - AssertSmallTitleBarWithoutButtons(); - else - AssertNoTitleBar(); + AssertNoTitleBar(); } protected override void VerifyMaximizedState() => AssertNoTitleBar(); - - private void AssertSmallTitleBarWithoutButtons() - { - var (titleBarHeight, buttonsHeight) = GetTitleBarInfo(); - Assert.True(titleBarHeight < 10); - Assert.NotEqual(0, titleBarHeight); - Assert.Equal(0, buttonsHeight); - } } public sealed class DecorationsNone : ExtendClientAreaWindowTests