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