Browse Source

Themeable Client Window Decorations (#20770)

* Implemented new drawn window decorations API

TODO: check if it works on Win32, bring back titlebar automation peer

* Adjusting naming a bit

* Naming / configuration changes

* Various fixes

* popover fix?

* wip

* Address review

* Extra window roles

* WIP

* Fixed drawn titlebar automation

* Purge ExtendClientAreaChromeHints.

* Fixed dynamically enabling drawn decorations

* api diff

* Add automation IDs for drawn decorations buttons

* Resolved the issues

* build

* Retry a few times when Pager isn't available after test is finished

* Only do faulty test detection if asked

* duplicate package reference

* Try disabling faulty tests on appium1

* Fix ExtendClientAreaWindowTests

* Apply initial button states

* Enable CSD shadow for X11

* net8?

* Address review

* more review comments

* Moar review comments

* Extra hit-test checks

* Moar review

* Prefix integration test app exitfullscreen to avoid clashes

* Disable drawn decorations if parts = None

* Respect SystemDecorations value on mac in extend-client-area mode

* Tidy up logic a bit

* Adjust win32 tests to titlebar not being in the tree when CSD are not enabled

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/20797/head
Nikita Tsukanov 3 weeks ago
committed by GitHub
parent
commit
3553eda8ae
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 100
      api/Avalonia.nupkg.xml
  2. 3
      native/Avalonia.Native/src/OSX/WindowImpl.h
  3. 44
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 1
      samples/ControlCatalog/MainWindow.xaml
  5. 9
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  6. 55
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  7. 3
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml
  8. 5
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs
  9. 2
      samples/IntegrationTestApp/Pages/WindowPage.axaml
  10. 12
      src/Avalonia.Base/Input/IInputRoot.cs
  11. 100
      src/Avalonia.Base/Input/WindowDecorationsElementRole.cs
  12. 2
      src/Avalonia.Base/Metadata/PrivateApiAttribute.cs
  13. 59
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  14. 7
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  15. 7
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  16. 26
      src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs
  17. 15
      src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs
  18. 2
      src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
  19. 2
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  20. 187
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  21. 41
      src/Avalonia.Controls/Chrome/DrawnWindowDecorationParts.cs
  22. 20
      src/Avalonia.Controls/Chrome/IWindowDrawnDecorationsTemplate.cs
  23. 106
      src/Avalonia.Controls/Chrome/ResizeGripLayer.cs
  24. 111
      src/Avalonia.Controls/Chrome/TitleBar.cs
  25. 27
      src/Avalonia.Controls/Chrome/WindowDecorationProperties.cs
  26. 590
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  27. 59
      src/Avalonia.Controls/Chrome/WindowDrawnDecorationsContent.cs
  28. 38
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  29. 13
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  30. 33
      src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs
  31. 37
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  32. 11
      src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
  33. 38
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  34. 4
      src/Avalonia.Controls/TopLevel.cs
  35. 252
      src/Avalonia.Controls/TopLevelHost.Decorations.cs
  36. 98
      src/Avalonia.Controls/TopLevelHost.Peers.cs
  37. 14
      src/Avalonia.Controls/TopLevelHost.cs
  38. 147
      src/Avalonia.Controls/Window.cs
  39. 5
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  40. 8
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  41. 19
      src/Avalonia.Native/WindowImpl.cs
  42. 10
      src/Avalonia.Native/avn.idl
  43. 4
      src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml
  44. 119
      src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml
  45. 3
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  46. 62
      src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml
  47. 215
      src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
  48. 4
      src/Avalonia.Themes.Simple/Accents/Base.xaml
  49. 126
      src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml
  50. 3
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  51. 69
      src/Avalonia.Themes.Simple/Controls/TitleBar.xaml
  52. 221
      src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml
  53. 22
      src/Avalonia.X11/X11Platform.cs
  54. 83
      src/Avalonia.X11/X11Window.cs
  55. 7
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  56. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  57. 21
      src/Markup/Avalonia.Markup.Xaml/Templates/WindowDrawnDecorationsTemplate.cs
  58. 32
      src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
  59. 26
      src/Windows/Avalonia.Win32/WindowImpl.cs
  60. 12
      tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
  61. 87
      tests/Avalonia.IntegrationTests.Appium/PointerTests_MacOS.cs
  62. 25
      tests/Avalonia.IntegrationTests.Appium/TestBase.cs
  63. 5
      tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs
  64. 50
      tests/Avalonia.IntegrationTests.Appium/WindowDecorationsTests.cs
  65. 10
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  66. 34
      tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs

100
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -253,6 +253,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Chrome.CaptionButtons</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Chrome.TitleBar</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.ContextRequestedEventArgs</Target>
@ -379,6 +391,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.ExtendClientAreaChromeHints</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.IApplicationPlatformEvents</Target>
@ -661,6 +679,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Chrome.CaptionButtons</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Chrome.TitleBar</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.ContextRequestedEventArgs</Target>
@ -787,6 +817,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.ExtendClientAreaChromeHints</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.IApplicationPlatformEvents</Target>
@ -1447,6 +1483,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.AppBuilder.get_LifetimeOverride</Target>
@ -1819,6 +1861,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[])</Target>
@ -1837,6 +1891,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean)</Target>
@ -2599,6 +2659,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.AppBuilder.get_LifetimeOverride</Target>
@ -2971,6 +3037,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[])</Target>
@ -2989,6 +3067,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean)</Target>
@ -3253,6 +3337,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize})</Target>
@ -3463,6 +3553,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
@ -4117,4 +4213,4 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

3
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;
};

44
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];

1
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}"

9
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -23,22 +23,13 @@
<DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
<CheckBox Content="Title Bar"
IsChecked="{Binding SystemTitleBarEnabled}"
DockPanel.Dock="Left" />
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
IsEnabled="{Binding SystemTitleBarEnabled}"
Margin="8,-10" />
</DockPanel>
<CheckBox Content="Prefer System Chrome"
IsChecked="{Binding PreferSystemChromeEnabled}"
IsEnabled="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Can Resize"
IsChecked="{Binding CanResize}" />

55
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<WindowState>();
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

3
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml

@ -6,9 +6,6 @@
x:Class="IntegrationTestApp.Pages.WindowDecorationsPage">
<StackPanel Spacing="4">
<CheckBox Name="WindowExtendClientAreaToDecorationsHint" Content="Extend Client Area to Decorations" />
<CheckBox Name="WindowForceSystemChrome" Content="Force SystemChrome" />
<CheckBox Name="WindowPreferSystemChrome" Content="Prefer SystemChrome" />
<CheckBox Name="WindowMacThickSystemChrome" Content="Mac Thick SystemChrome" />
<CheckBox Name="WindowShowTitleAreaControl" Content="Show Title Area Control" />
<TextBox Name="WindowTitleBarHeightHint" Text="-1" PlaceholderText="In dips" />
<Button Name="ApplyWindowDecorations"

5
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs

@ -1,7 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
namespace IntegrationTestApp.Pages;
@ -17,10 +16,6 @@ public partial class WindowDecorationsPage : UserControl
window.ExtendClientAreaToDecorationsHint = WindowExtendClientAreaToDecorationsHint.IsChecked!.Value;
window.ExtendClientAreaTitleBarHeightHint =
int.TryParse(WindowTitleBarHeightHint.Text, out var val) ? val / window.DesktopScaling : -1;
window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome
| (WindowForceSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.SystemChrome : 0)
| (WindowPreferSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.PreferSystemChrome : 0)
| (WindowMacThickSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.OSXThickTitleBar : 0);
if (window is ShowWindowTest showWindowTest && WindowShowTitleAreaControl.IsChecked == true)
{

2
samples/IntegrationTestApp/Pages/WindowPage.axaml

@ -37,7 +37,7 @@
<Button Name="ShowWindow" Click="ShowWindow_Click">Show Window</Button>
<Button Name="SendToBack" Click="SendToBack_Click">Send to Back</Button>
<Button Name="EnterFullscreen" Click="EnterFullscreen_Click">Enter Fullscreen</Button>
<Button Name="ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button>
<Button Name="ExitFullscreen" AutomationProperties.AutomationId="IntegrationTestApp_ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button>
<Button Name="RestoreAll" Click="RestoreAll_Click">Restore All</Button>
<Button Name="ShowTopmostWindow" Click="ShowTopmostWindow_Click">Show Topmost Window</Button>
<Button Name="ShowTransparentWindow" Click="ShowTransparentWindow_Click">Transparent Window</Button>

12
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; }
/// <summary>
/// Performs a hit-test for chrome/decoration elements at the given position.
/// </summary>
/// <param name="point">The point in root-relative coordinates.</param>
/// <returns>
/// <c>null</c> if no chrome element was hit (no chrome involvement at this point).
/// <see cref="WindowDecorationsElementRole.DecorationsElement"/> or <see cref="WindowDecorationsElementRole.User"/>
/// if an interactive chrome element was hit — the platform should redirect non-client input to regular client input.
/// Any other non-<see cref="WindowDecorationsElementRole.None"/> value indicates a specific non-client role (titlebar, resize grip, etc.).
/// </returns>
internal WindowDecorationsElementRole? HitTestChromeElement(Point point) => null;
}
}

100
src/Avalonia.Base/Input/WindowDecorationsElementRole.cs

@ -0,0 +1,100 @@
namespace Avalonia.Input;
/// <summary>
/// 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.
/// </summary>
public enum WindowDecorationsElementRole
{
/// <summary>
/// No special role. The element is invisible to chrome hit-testing.
/// </summary>
None,
/// <summary>
/// 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.
/// </summary>
DecorationsElement,
/// <summary>
/// An interactive element set by user code that should receive input even when
/// overlapping chrome areas. Has the same effect as <see cref="DecorationsElement"/>
/// but is intended for use by application developers.
/// </summary>
User,
/// <summary>
/// The element acts as a titlebar drag area.
/// Clicking and dragging on this element initiates a platform window move.
/// </summary>
TitleBar,
/// <summary>
/// Resize grip for the north (top) edge.
/// </summary>
ResizeN,
/// <summary>
/// Resize grip for the south (bottom) edge.
/// </summary>
ResizeS,
/// <summary>
/// Resize grip for the east (right) edge.
/// </summary>
ResizeE,
/// <summary>
/// Resize grip for the west (left) edge.
/// </summary>
ResizeW,
/// <summary>
/// Resize grip for the northeast corner.
/// </summary>
ResizeNE,
/// <summary>
/// Resize grip for the northwest corner.
/// </summary>
ResizeNW,
/// <summary>
/// Resize grip for the southeast corner.
/// </summary>
ResizeSE,
/// <summary>
/// Resize grip for the southwest corner.
/// </summary>
ResizeSW,
/// <summary>
/// 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.
/// </summary>
CloseButton,
/// <summary>
/// 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.
/// </summary>
MinimizeButton,
/// <summary>
/// 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.
/// </summary>
MaximizeButton,
/// <summary>
/// The element acts as the window fullscreen toggle button.
/// Treated as an interactive decoration element on all platforms.
/// </summary>
FullScreenButton
}

2
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
{

59
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -93,6 +93,29 @@ namespace Avalonia.Automation
"ControlTypeOverride",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.ClassNameOverride attached property.
/// </summary>
/// <remarks>
/// This property affects the default value for <see cref="AutomationPeer.GetClassName"/>.
/// </remarks>
public static readonly AttachedProperty<string?> ClassNameOverrideProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string?>(
"ClassNameOverride",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.IsControlElementOverride attached property.
/// </summary>
/// <remarks>
/// This property affects the default value for
/// <see cref="AutomationPeer.IsControlElement"/>.
/// </remarks>
public static readonly AttachedProperty<bool?> IsControlElementOverrideProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool?>(
"IsControlElementOverride",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.HelpText attached property.
/// </summary>
@ -352,6 +375,42 @@ namespace Avalonia.Automation
return element.GetValue(ControlTypeOverrideProperty);
}
/// <summary>
/// Helper for setting the value of the <see cref="ClassNameOverrideProperty"/> on a StyledElement.
/// </summary>
public static void SetClassNameOverride(StyledElement element, string? value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(ClassNameOverrideProperty, value);
}
/// <summary>
/// Helper for reading the value of the <see cref="ClassNameOverrideProperty"/> on a StyledElement.
/// </summary>
public static string? GetClassNameOverride(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
return element.GetValue(ClassNameOverrideProperty);
}
/// <summary>
/// Helper for setting the value of the <see cref="IsControlElementOverrideProperty"/> on a StyledElement.
/// </summary>
public static void SetIsControlElementOverride(StyledElement element, bool? value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(IsControlElementOverrideProperty, value);
}
/// <summary>
/// Helper for reading the value of the <see cref="IsControlElementOverrideProperty"/> on a StyledElement.
/// </summary>
public static bool? GetIsControlElementOverride(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
return element.GetValue(IsControlElementOverrideProperty);
}
/// <summary>
/// Helper for setting the value of the <see cref="HelpTextProperty"/> on a StyledElement.
/// </summary>

7
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -206,7 +206,7 @@ namespace Avalonia.Automation.Peers
/// </item>
/// </list>
/// </remarks>
public string GetClassName() => GetClassNameCore() ?? string.Empty;
public string GetClassName() => GetClassNameOverrideCore() ?? string.Empty;
/// <summary>
/// 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;

7
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;
}

26
src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs

@ -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;
}
}

15
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<AutomationPeer>? GetChildrenCore()
{
var baseChildren = base.GetChildrenCore();
var overlayPeer = Owner.TopLevelHost.GetOrCreateDecorationsOverlaysPeer();
var rv = new List<AutomationPeer> { 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;

2
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.
/// </remarks>
[PrivateApi]
public interface IEmbeddedRootProvider
{
/// <summary>

2
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
/// <see cref="IEmbeddedRootProvider"/> instead.
/// </remarks>
[PrivateApi]
public interface IRootProvider
{
/// <summary>

187
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -1,187 +0,0 @@
using System;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Chrome
{
/// <summary>
/// Draws window minimize / maximize / close buttons in a <see cref="TitleBar"/> when managed client decorations are enabled.
/// </summary>
[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;
/// <summary>
/// Currently attached window.
/// </summary>
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<Button>(PART_CloseButton) is { } closeButton)
{
closeButton.Click += (_, args) =>
{
OnClose();
args.Handled = true;
};
}
if (e.NameScope.Find<Button>(PART_RestoreButton) is { } restoreButton)
{
restoreButton.Click += (_, args) =>
{
OnRestore();
args.Handled = true;
};
_restoreButton = restoreButton;
UpdateRestoreButtonState();
}
if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton)
{
minimizeButton.Click += (_, args) =>
{
OnMinimize();
args.Handled = true;
};
_minimizeButton = minimizeButton;
UpdateMinimizeButtonState();
}
if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton)
{
fullScreenButton.Click += (_, args) =>
{
OnToggleFullScreen();
args.Handled = true;
};
_fullScreenButton = fullScreenButton;
UpdateFullScreenButtonState();
}
}
private void UpdateRestoreButtonState()
{
if (_restoreButton is null)
return;
_restoreButton.IsEnabled = HostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => HostWindow.CanResize,
WindowState.Normal => HostWindow.CanMaximize,
_ => true
};
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton is null)
return;
_minimizeButton.IsEnabled = HostWindow?.CanMinimize ?? true;
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton is null)
return;
_fullScreenButton.IsEnabled = HostWindow?.WindowState == WindowState.FullScreen ?
HostWindow.CanResize :
HostWindow?.CanMaximize ?? true;
}
}
}

41
src/Avalonia.Controls/Chrome/DrawnWindowDecorationParts.cs

@ -0,0 +1,41 @@
using System;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Flags controlling which parts of drawn window decorations are active.
/// Set by Window based on platform capabilities and user preferences.
/// </summary>
[Flags]
internal enum DrawnWindowDecorationParts
{
/// <summary>
/// No decoration parts are active.
/// </summary>
None = 0,
/// <summary>
/// Shadow/outer area is active.
/// </summary>
Shadow = 1,
/// <summary>
/// Frame border is active.
/// </summary>
Border = 2,
/// <summary>
/// Titlebar is active.
/// </summary>
TitleBar = 4,
/// <summary>
/// Resize grips are active.
/// </summary>
ResizeGrips = 8,
/// <summary>
/// All decoration parts are active.
/// </summary>
All = Shadow | Border | TitleBar | ResizeGrips
}

20
src/Avalonia.Controls/Chrome/IWindowDrawnDecorationsTemplate.cs

@ -0,0 +1,20 @@
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Interface for a template that produces <see cref="WindowDrawnDecorationsContent"/>.
/// Implemented by the XAML template class in Avalonia.Markup.Xaml.
/// Extends <see cref="ITemplate"/> so the XAML compiler assigns the template object directly
/// instead of auto-calling Build().
/// </summary>
[ControlTemplateScope]
public interface IWindowDrawnDecorationsTemplate : ITemplate
{
/// <summary>
/// Builds the template and returns the content with its name scope.
/// </summary>
new TemplateResult<WindowDrawnDecorationsContent> Build();
}

106
src/Avalonia.Controls/Chrome/ResizeGripLayer.cs

@ -0,0 +1,106 @@
using System;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// An invisible layer that provides resize grip hit-test zones at window edges.
/// Grips only cover the frame/shadow area outside the client area.
/// </summary>
internal class ResizeGripLayer : Control
{
private readonly Control _top = CreateGrip(WindowDecorationsElementRole.ResizeN, StandardCursorType.TopSide);
private readonly Control _bottom = CreateGrip(WindowDecorationsElementRole.ResizeS, StandardCursorType.BottomSide);
private readonly Control _left = CreateGrip(WindowDecorationsElementRole.ResizeW, StandardCursorType.LeftSide);
private readonly Control _right = CreateGrip(WindowDecorationsElementRole.ResizeE, StandardCursorType.RightSide);
private readonly Control _topLeft = CreateGrip(WindowDecorationsElementRole.ResizeNW, StandardCursorType.TopLeftCorner);
private readonly Control _topRight = CreateGrip(WindowDecorationsElementRole.ResizeNE, StandardCursorType.TopRightCorner);
private readonly Control _bottomLeft = CreateGrip(WindowDecorationsElementRole.ResizeSW, StandardCursorType.BottomLeftCorner);
private readonly Control _bottomRight = CreateGrip(WindowDecorationsElementRole.ResizeSE, StandardCursorType.BottomRightCorner);
private Thickness _gripThickness;
public ResizeGripLayer()
{
IsHitTestVisible = true;
VisualChildren.Add(_top);
VisualChildren.Add(_bottom);
VisualChildren.Add(_left);
VisualChildren.Add(_right);
VisualChildren.Add(_topLeft);
VisualChildren.Add(_topRight);
VisualChildren.Add(_bottomLeft);
VisualChildren.Add(_bottomRight);
}
/// <summary>
/// The thickness of the resize grip area at each edge.
/// Grips are placed outside the client area (covering frame + shadow).
/// </summary>
internal Thickness GripThickness
{
get => _gripThickness;
set
{
if (_gripThickness != value)
{
_gripThickness = value;
InvalidateArrange();
}
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var gt = _gripThickness;
var w = finalSize.Width;
var h = finalSize.Height;
// Hide all grips when thickness is zero (e.g. maximized/fullscreen)
var hasGrips = gt.Left > 0 || gt.Top > 0 || gt.Right > 0 || gt.Bottom > 0;
IsHitTestVisible = hasGrips;
if (!hasGrips)
{
var empty = new Rect();
_top.Arrange(empty);
_bottom.Arrange(empty);
_left.Arrange(empty);
_right.Arrange(empty);
_topLeft.Arrange(empty);
_topRight.Arrange(empty);
_bottomLeft.Arrange(empty);
_bottomRight.Arrange(empty);
return finalSize;
}
// Edges fill the space between their adjacent corners
_top.Arrange(new Rect(gt.Left, 0, Math.Max(0, w - gt.Left - gt.Right), gt.Top));
_bottom.Arrange(new Rect(gt.Left, h - gt.Bottom, Math.Max(0, w - gt.Left - gt.Right), gt.Bottom));
_left.Arrange(new Rect(0, gt.Top, gt.Left, Math.Max(0, h - gt.Top - gt.Bottom)));
_right.Arrange(new Rect(w - gt.Right, gt.Top, gt.Right, Math.Max(0, h - gt.Top - gt.Bottom)));
// Corners use the thickness of their adjacent edges
_topLeft.Arrange(new Rect(0, 0, gt.Left, gt.Top));
_topRight.Arrange(new Rect(w - gt.Right, 0, gt.Right, gt.Top));
_bottomLeft.Arrange(new Rect(0, h - gt.Bottom, gt.Left, gt.Bottom));
_bottomRight.Arrange(new Rect(w - gt.Right, h - gt.Bottom, gt.Right, gt.Bottom));
return finalSize;
}
private static Control CreateGrip(WindowDecorationsElementRole role, StandardCursorType cursorType)
{
var grip = new Border
{
Background = Brushes.Transparent,
Cursor = new Cursor(cursorType),
IsHitTestVisible = true,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
};
WindowDecorationProperties.SetElementRole(grip, role);
return grip;
}
}

111
src/Avalonia.Controls/Chrome/TitleBar.cs

@ -1,111 +0,0 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Chrome
{
/// <summary>
/// Draws a titlebar when managed client decorations are enabled.
/// </summary>
[TemplatePart("PART_CaptionButtons", typeof(CaptionButtons), IsRequired = true)]
[PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
public class TitleBar : TemplatedControl
{
private CompositeDisposable? _disposables;
private CaptionButtons? _captionButtons;
private void UpdateSize(Window window)
{
Margin = new Thickness(
window.OffScreenMargin.Left,
window.OffScreenMargin.Top,
window.OffScreenMargin.Right,
window.OffScreenMargin.Bottom);
if (window.WindowState != WindowState.FullScreen)
{
var height = Math.Max(0, window.WindowDecorationMargin.Top);
Height = height;
_captionButtons?.Height = window.SystemDecorations == SystemDecorations.Full ? height : 0;
}
else
{
// Note: apparently the titlebar was supposed to be displayed when hovering the top of the screen,
// to mimic macOS behavior. This has been broken for years. It actually only partially works if the
// window is FullScreen right on startup, and only once. Any size change will then break it.
// Disable it for now.
// TODO: restore that behavior so that it works in all cases
Height = 0;
_captionButtons?.Height = 0;
}
IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false;
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_captionButtons?.Detach();
_captionButtons = e.NameScope.Get<CaptionButtons>("PART_CaptionButtons");
if (TopLevel.GetTopLevel(this) is Window window)
{
_captionButtons?.Attach(window);
UpdateSize(window);
}
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (TopLevel.GetTopLevel(this) is Window window)
{
_disposables = new CompositeDisposable(6)
{
window.GetObservable(Window.WindowDecorationMarginProperty)
.Subscribe(_ => UpdateSize(window)),
window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty)
.Subscribe(_ => UpdateSize(window)),
window.GetObservable(Window.OffScreenMarginProperty)
.Subscribe(_ => UpdateSize(window)),
window.GetObservable(Window.ExtendClientAreaChromeHintsProperty)
.Subscribe(_ => UpdateSize(window)),
window.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);
UpdateSize(window);
}),
window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
.Subscribe(_ => UpdateSize(window))
};
}
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_disposables?.Dispose();
_captionButtons?.Detach();
_captionButtons = null;
}
/// <inheritdoc />
protected override AutomationPeer OnCreateAutomationPeer() => new TitleBarAutomationPeer(this);
}
}

27
src/Avalonia.Controls/Chrome/WindowDecorationProperties.cs

@ -0,0 +1,27 @@
using Avalonia.Input;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Provides attached properties for window decoration hit-testing.
/// </summary>
public static class WindowDecorationProperties
{
/// <summary>
/// Defines the <see cref="WindowDecorationsElementRole"/> attached property.
/// Marks a visual element with a specific role for non-client hit-testing.
/// Can be applied to any element in the visual tree, not limited to decoration children.
/// </summary>
public static readonly AttachedProperty<WindowDecorationsElementRole> ElementRoleProperty =
AvaloniaProperty.RegisterAttached<Visual, WindowDecorationsElementRole>("ElementRole", typeof(WindowDecorationProperties));
/// <summary>
/// Gets the <see cref="WindowDecorationsElementRole"/> for the specified element.
/// </summary>
public static WindowDecorationsElementRole GetElementRole(Visual element) => element.GetValue(ElementRoleProperty);
/// <summary>
/// Sets the <see cref="WindowDecorationsElementRole"/> for the specified element.
/// </summary>
public static void SetElementRole(Visual element, WindowDecorationsElementRole value) => element.SetValue(ElementRoleProperty, value);
}

590
src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs

@ -0,0 +1,590 @@
using System;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.Styling;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Manages client-side window decorations (app-drawn window frame).
/// This is a logical element that holds the decorations template and properties.
/// TopLevelHost extracts overlay/underlay/popover visuals from the template content
/// and inserts them into its own visual tree.
/// </summary>
[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar)]
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_MinimizeButton, typeof(Button))]
[TemplatePart(PART_MaximizeButton, typeof(Button))]
[TemplatePart(PART_FullScreenButton, typeof(Button))]
[TemplatePart(PART_PopoverCloseButton, typeof(Button))]
[TemplatePart(PART_PopoverFullScreenButton, typeof(Button))]
[TemplatePart(PART_TitleBar, typeof(Panel))]
public class WindowDrawnDecorations : StyledElement
{
internal const string pcNormal = ":normal";
internal const string pcMaximized = ":maximized";
internal const string pcFullscreen = ":fullscreen";
internal const string pcHasShadow = ":has-shadow";
internal const string pcHasBorder = ":has-border";
internal const string pcHasTitlebar = ":has-titlebar";
// Template part names for caption buttons
internal const string PART_CloseButton = "PART_CloseButton";
internal const string PART_MinimizeButton = "PART_MinimizeButton";
internal const string PART_MaximizeButton = "PART_MaximizeButton";
internal const string PART_FullScreenButton = "PART_FullScreenButton";
// Popover caption buttons (separate names to avoid name scope conflicts)
internal const string PART_PopoverCloseButton = "PART_PopoverCloseButton";
internal const string PART_PopoverFullScreenButton = "PART_PopoverFullScreenButton";
// Titlebar panel
internal const string PART_TitleBar = "PART_TitleBar";
/// <summary>
/// Defines the <see cref="Template"/> property.
/// </summary>
public static readonly StyledProperty<IWindowDrawnDecorationsTemplate?> TemplateProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, IWindowDrawnDecorationsTemplate?>(nameof(Template));
/// <summary>
/// Defines the <see cref="DefaultTitleBarHeight"/> property.
/// </summary>
public static readonly StyledProperty<double> DefaultTitleBarHeightProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, double>(nameof(DefaultTitleBarHeight));
/// <summary>
/// Defines the <see cref="DefaultFrameThickness"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> DefaultFrameThicknessProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultFrameThickness));
/// <summary>
/// Defines the <see cref="DefaultShadowThickness"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> DefaultShadowThicknessProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, Thickness>(nameof(DefaultShadowThickness));
/// <summary>
/// Defines the <see cref="TitleBarHeight"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, double> TitleBarHeightProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, double>(
nameof(TitleBarHeight),
o => o.TitleBarHeight);
/// <summary>
/// Defines the <see cref="FrameThickness"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, Thickness> FrameThicknessProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, Thickness>(
nameof(FrameThickness),
o => o.FrameThickness);
/// <summary>
/// Defines the <see cref="ShadowThickness"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, Thickness> ShadowThicknessProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, Thickness>(
nameof(ShadowThickness),
o => o.ShadowThickness);
/// <summary>
/// Defines the <see cref="HasShadow"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, bool> HasShadowProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, bool>(
nameof(HasShadow),
o => o.HasShadow);
/// <summary>
/// Defines the <see cref="HasBorder"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, bool> HasBorderProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, bool>(
nameof(HasBorder),
o => o.HasBorder);
/// <summary>
/// Defines the <see cref="HasTitleBar"/> property.
/// </summary>
public static readonly DirectProperty<WindowDrawnDecorations, bool> HasTitleBarProperty =
AvaloniaProperty.RegisterDirect<WindowDrawnDecorations, bool>(
nameof(HasTitleBar),
o => o.HasTitleBar);
/// <summary>
/// Defines the <see cref="Title"/> property.
/// </summary>
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, string?>(nameof(Title));
/// <summary>
/// Defines the <see cref="EnabledParts"/> property.
/// </summary>
internal static readonly StyledProperty<DrawnWindowDecorationParts> EnabledPartsProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, DrawnWindowDecorationParts>(nameof(EnabledParts),
defaultValue: DrawnWindowDecorationParts.None);
private IWindowDrawnDecorationsTemplate? _appliedTemplate;
private INameScope? _templateNameScope;
private Button? _closeButton;
private Button? _minimizeButton;
private Button? _maximizeButton;
private Button? _fullScreenButton;
private Button? _popoverCloseButton;
private Button? _popoverFullScreenButton;
private IDisposable? _windowSubscriptions;
private Window? _hostWindow;
private double _titleBarHeightOverride = -1;
/// <summary>
/// Raised when any property affecting the effective geometry changes
/// (effective titlebar height, frame thickness, or shadow thickness).
/// </summary>
internal event Action? EffectiveGeometryChanged;
/// <summary>
/// Gets or sets the decorations template.
/// </summary>
public IWindowDrawnDecorationsTemplate? Template
{
get => GetValue(TemplateProperty);
set => SetValue(TemplateProperty, value);
}
/// <summary>
/// Gets or sets the theme-set default titlebar height.
/// </summary>
public double DefaultTitleBarHeight
{
get => GetValue(DefaultTitleBarHeightProperty);
set => SetValue(DefaultTitleBarHeightProperty, value);
}
/// <summary>
/// Gets or sets the theme-set default frame thickness.
/// </summary>
public Thickness DefaultFrameThickness
{
get => GetValue(DefaultFrameThicknessProperty);
set => SetValue(DefaultFrameThicknessProperty, value);
}
/// <summary>
/// Gets or sets the theme-set default shadow thickness.
/// </summary>
public Thickness DefaultShadowThickness
{
get => GetValue(DefaultShadowThicknessProperty);
set => SetValue(DefaultShadowThicknessProperty, value);
}
/// <summary>
/// Gets or sets the titlebar height override.
/// When -1, falls back to <see cref="DefaultTitleBarHeight"/>.
/// </summary>
internal double TitleBarHeightOverride
{
get => _titleBarHeightOverride;
set { _titleBarHeightOverride = value; UpdateEffectiveGeometry(); }
}
/// <summary>
/// Gets or sets the frame thickness override.
/// When set, takes precedence over <see cref="DefaultFrameThickness"/>.
/// </summary>
internal Thickness? FrameThicknessOverride
{
get;
set
{
field = value;
UpdateEffectiveGeometry();
}
}
/// <summary>
/// Gets or sets the shadow thickness override.
/// When set, takes precedence over <see cref="DefaultShadowThickness"/>.
/// </summary>
internal Thickness? ShadowThicknessOverride
{
get;
set
{
field = value;
UpdateEffectiveGeometry();
}
}
/// <summary>
/// Gets or sets the window title displayed in the decorations.
/// </summary>
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets or sets which decoration parts are enabled.
/// Set by Window based on platform capabilities and user preferences.
/// </summary>
internal DrawnWindowDecorationParts EnabledParts
{
get => GetValue(EnabledPartsProperty);
set => SetValue(EnabledPartsProperty, value);
}
/// <summary>
/// Gets the built template content.
/// </summary>
public WindowDrawnDecorationsContent? Content { get; private set; }
/// <summary>
/// Gets the effective titlebar height, resolving -1 override to the default.
/// Returns 0 if titlebar part is disabled.
/// </summary>
public double TitleBarHeight
{
get;
private set => SetAndRaise(TitleBarHeightProperty, ref field, value);
}
/// <summary>
/// Gets the effective frame thickness.
/// Uses FrameThicknessOverride if explicitly set, otherwise DefaultFrameThickness.
/// Returns zero if border part is disabled.
/// </summary>
public Thickness FrameThickness
{
get;
private set => SetAndRaise(FrameThicknessProperty, ref field, value);
}
/// <summary>
/// Gets the effective shadow thickness.
/// Uses ShadowThicknessOverride if explicitly set, otherwise DefaultShadowThickness.
/// Returns zero if shadow part is disabled.
/// </summary>
public Thickness ShadowThickness
{
get;
private set => SetAndRaise(ShadowThicknessProperty, ref field, value);
}
/// <summary>
/// Gets a value indicating whether the shadow decoration part is enabled.
/// </summary>
public bool HasShadow
{
get;
private set => SetAndRaise(HasShadowProperty, ref field, value);
}
/// <summary>
/// Gets a value indicating whether the border decoration part is enabled.
/// </summary>
public bool HasBorder
{
get;
private set => SetAndRaise(HasBorderProperty, ref field, value);
}
/// <summary>
/// Gets a value indicating whether the title bar decoration part is enabled.
/// </summary>
public bool HasTitleBar
{
get;
private set => SetAndRaise(HasTitleBarProperty, ref field, value);
}
static WindowDrawnDecorations()
{
TemplateProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.InvalidateTemplate());
EnabledPartsProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) =>
{
x.UpdateEnabledPartsPseudoClasses();
x.UpdateEffectiveGeometry();
});
DefaultTitleBarHeightProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
DefaultFrameThicknessProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
DefaultShadowThicknessProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
}
/// <summary>
/// Applies the template if it has changed.
/// </summary>
internal void ApplyTemplate()
{
var template = Template;
if (template == _appliedTemplate)
return;
// Clean up old content
if (Content != null)
{
DetachCaptionButtons();
LogicalChildren.Remove(Content);
((ISetLogicalParent)Content).SetParent(null);
Content = null;
_templateNameScope = null;
}
_appliedTemplate = template;
if (template == null)
return;
var result = template.Build();
Content = result.Result;
_templateNameScope = result.NameScope;
if (Content != null)
{
TemplatedControl.ApplyTemplatedParent(Content, this);
LogicalChildren.Add(Content);
((ISetLogicalParent)Content).SetParent(this);
}
AttachCaptionButtons();
}
/// <summary>
/// Attaches to the specified window for caption button interactions and state tracking.
/// </summary>
internal void Attach(Window window)
{
if (_hostWindow == window)
return;
Detach();
_hostWindow = window;
_windowSubscriptions = new CompositeDisposable
{
window.GetObservable(Window.TitleProperty)
.Subscribe(title => SetCurrentValue(TitleProperty, title)),
window.GetObservable(Window.CanMaximizeProperty)
.Subscribe(_ =>
{
UpdateMaximizeButtonState();
UpdateFullScreenButtonState();
}),
window.GetObservable(Window.CanMinimizeProperty)
.Subscribe(_ => UpdateMinimizeButtonState()),
window.GetObservable(Window.WindowStateProperty)
.Subscribe(state =>
{
PseudoClasses.Set(pcNormal, state == WindowState.Normal);
PseudoClasses.Set(pcMaximized, state == WindowState.Maximized);
PseudoClasses.Set(pcFullscreen, state == WindowState.FullScreen);
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}),
};
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}
/// <summary>
/// Detaches from the current window.
/// </summary>
internal void Detach()
{
_windowSubscriptions?.Dispose();
_windowSubscriptions = null;
_hostWindow = null;
}
private void InvalidateTemplate()
{
_appliedTemplate = null;
}
private void AttachCaptionButtons()
{
if (_templateNameScope == null)
return;
_closeButton = _templateNameScope.Find<Button>(PART_CloseButton);
_minimizeButton = _templateNameScope.Find<Button>(PART_MinimizeButton);
_maximizeButton = _templateNameScope.Find<Button>(PART_MaximizeButton);
_fullScreenButton = _templateNameScope.Find<Button>(PART_FullScreenButton);
_popoverCloseButton = _templateNameScope.Find<Button>(PART_PopoverCloseButton);
_popoverFullScreenButton = _templateNameScope.Find<Button>(PART_PopoverFullScreenButton);
var titleBar = _templateNameScope.Find<Control>(PART_TitleBar);
if (titleBar != null)
{
AutomationProperties.SetIsControlElementOverride(titleBar, true);
AutomationProperties.SetAutomationId(titleBar, "AvaloniaTitleBar");
AutomationProperties.SetName(titleBar, "TitleBar");
}
if (_closeButton != null)
{
AutomationProperties.SetAutomationId(_closeButton, "Close");
AutomationProperties.SetName(_closeButton, "Close");
_closeButton.Click += OnCloseButtonClick;
}
if (_minimizeButton != null)
{
AutomationProperties.SetAutomationId(_minimizeButton, "Minimize");
AutomationProperties.SetName(_minimizeButton, "Minimize");
_minimizeButton.Click += OnMinimizeButtonClick;
UpdateMinimizeButtonState();
}
if (_maximizeButton != null)
{
_maximizeButton.Click += OnMaximizeButtonClick;
AutomationProperties.SetAutomationId(_maximizeButton, "Maximize");
AutomationProperties.SetName(_maximizeButton, "Maximize");
UpdateMaximizeButtonState();
}
if (_fullScreenButton != null)
{
_fullScreenButton.Click += OnFullScreenButtonClick;
AutomationProperties.SetAutomationId(_fullScreenButton, "Fullscreen");
AutomationProperties.SetName(_fullScreenButton, "Fullscreen");
UpdateFullScreenButtonState();
}
if (_popoverCloseButton != null)
{
_popoverCloseButton.Click += OnCloseButtonClick;
AutomationProperties.SetAutomationId(_popoverCloseButton, "FullscreenClose");
AutomationProperties.SetName(_popoverCloseButton, "Close");
}
if (_popoverFullScreenButton != null)
{
_popoverFullScreenButton.Click += OnFullScreenButtonClick;
AutomationProperties.SetAutomationId(_popoverFullScreenButton, "ExitFullscreen");
AutomationProperties.SetName(_popoverFullScreenButton, "ExitFullscreen");
}
}
private void DetachCaptionButtons()
{
if (_closeButton != null)
_closeButton.Click -= OnCloseButtonClick;
if (_minimizeButton != null)
_minimizeButton.Click -= OnMinimizeButtonClick;
if (_maximizeButton != null)
_maximizeButton.Click -= OnMaximizeButtonClick;
if (_fullScreenButton != null)
_fullScreenButton.Click -= OnFullScreenButtonClick;
if (_popoverCloseButton != null)
_popoverCloseButton.Click -= OnCloseButtonClick;
if (_popoverFullScreenButton != null)
_popoverFullScreenButton.Click -= OnFullScreenButtonClick;
_closeButton = null;
_minimizeButton = null;
_maximizeButton = null;
_fullScreenButton = null;
_popoverCloseButton = null;
_popoverFullScreenButton = null;
}
private void OnCloseButtonClick(object? sender, Interactivity.RoutedEventArgs e)
{
_hostWindow?.Close();
e.Handled = true;
}
private void OnMinimizeButtonClick(object? sender, Interactivity.RoutedEventArgs e)
{
if (_hostWindow != null)
_hostWindow.WindowState = WindowState.Minimized;
e.Handled = true;
}
private void OnMaximizeButtonClick(object? sender, Interactivity.RoutedEventArgs e)
{
if (_hostWindow != null)
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
e.Handled = true;
}
private void OnFullScreenButtonClick(object? sender, Interactivity.RoutedEventArgs e)
{
if (_hostWindow != null)
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen
? WindowState.Normal
: WindowState.FullScreen;
e.Handled = true;
}
private void UpdateMaximizeButtonState()
{
if (_maximizeButton == null)
return;
_maximizeButton.IsEnabled = _hostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize,
WindowState.Normal => _hostWindow.CanMaximize,
_ => true
};
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton == null)
return;
_minimizeButton.IsEnabled = _hostWindow?.CanMinimize ?? true;
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton == null)
return;
_fullScreenButton.IsEnabled = _hostWindow?.WindowState == WindowState.FullScreen
? _hostWindow.CanResize
: _hostWindow?.CanMaximize ?? true;
}
private void UpdateEffectiveGeometry()
{
TitleBarHeight = EnabledParts.HasFlag(DrawnWindowDecorationParts.TitleBar)
? (TitleBarHeightOverride == -1 ? DefaultTitleBarHeight : TitleBarHeightOverride)
: 0;
FrameThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Border)
? (FrameThicknessOverride ?? DefaultFrameThickness)
: default;
ShadowThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Shadow)
? (ShadowThicknessOverride ?? DefaultShadowThickness)
: default;
EffectiveGeometryChanged?.Invoke();
}
private void UpdateEnabledPartsPseudoClasses()
{
var parts = EnabledParts;
var hasShadow = parts.HasFlag(DrawnWindowDecorationParts.Shadow);
var hasBorder = parts.HasFlag(DrawnWindowDecorationParts.Border);
var hasTitleBar = parts.HasFlag(DrawnWindowDecorationParts.TitleBar);
HasShadow = hasShadow;
HasBorder = hasBorder;
HasTitleBar = hasTitleBar;
PseudoClasses.Set(pcHasShadow, hasShadow);
PseudoClasses.Set(pcHasBorder, hasBorder);
PseudoClasses.Set(pcHasTitlebar, hasTitleBar);
}
}

59
src/Avalonia.Controls/Chrome/WindowDrawnDecorationsContent.cs

@ -0,0 +1,59 @@
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Holds the template content for <see cref="WindowDrawnDecorations"/>.
/// Contains three visual slots: Underlay, Overlay, and FullscreenPopover.
/// </summary>
public class WindowDrawnDecorationsContent : StyledElement
{
/// <summary>
/// Gets or sets the overlay layer content (titlebar, caption buttons).
/// Positioned above the client area.
/// </summary>
public Control? Overlay
{
get => field;
set => HandleLogicalChild(ref field, value);
}
/// <summary>
/// Gets or sets the underlay layer content (borders, background, shadow area).
/// Positioned below the client area.
/// </summary>
public Control? Underlay
{
get => field;
set => HandleLogicalChild(ref field, value);
}
/// <summary>
/// Gets or sets the fullscreen popover content.
/// Shown when the user hovers the pointer at the top of the window in fullscreen mode.
/// </summary>
public Control? FullscreenPopover
{
get => field;
set => HandleLogicalChild(ref field, value);
}
private void HandleLogicalChild(ref Control? field, Control? value)
{
if (field == value)
return;
if (field != null)
{
LogicalChildren.Remove(field);
((ISetLogicalParent)field).SetParent(null);
}
field = value;
if (field != null)
{
LogicalChildren.Add(field);
((ISetLogicalParent)field).SetParent(this);
}
}
}

38
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@ -1,38 +0,0 @@
using System;
namespace Avalonia.Platform
{
/// <summary>
/// Hint for Window Chrome when ClientArea is Extended.
/// </summary>
[Flags]
public enum ExtendClientAreaChromeHints
{
/// <summary>
/// There will be no chrome at all.
/// </summary>
NoChrome,
/// <summary>
/// The default for the platform.
/// </summary>
Default = PreferSystemChrome,
/// <summary>
/// Use SystemChrome
/// </summary>
SystemChrome = 0x01,
/// <summary>
/// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used.
/// This is because Windows Chrome can not be shown on top of user content.
/// </summary>
PreferSystemChrome = 0x02,
/// <summary>
/// On OSX the titlebar is the thicker toolbar kind. Causes traffic lights to be positioned
/// slightly lower than normal.
/// </summary>
OSXThickTitleBar = 0x08,
}
}

13
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Metadata;
@ -96,6 +97,12 @@ namespace Avalonia.Platform
/// </summary>
bool NeedsManagedDecorations { get; }
/// <summary>
/// Gets flags indicating which drawn decoration parts the platform requires.
/// For example, X11 needs shadow, border, and resize grips; Win32 only needs titlebar/buttons.
/// </summary>
PlatformRequestedDrawnDecoration RequestedDrawnDecorations { get; }
/// <summary>
/// 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
/// <param name="extendIntoClientAreaHint">true to enable, false to disable</param>
void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint);
/// <summary>
/// Sets hints that configure how the client area extends.
/// </summary>
/// <param name="hints"></param>
void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints);
/// <summary>
/// Sets how big the non-client titlebar area should be.
/// </summary>

33
src/Avalonia.Controls/Platform/PlatformRequestedDrawnDecoration.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Controls.Platform;
/// <summary>
/// Flags indicating which drawn decoration parts a platform backend requires.
/// </summary>
[Flags, PrivateApi]
public enum PlatformRequestedDrawnDecoration
{
None = 0,
/// <summary>
/// Platform needs app-drawn window shadow.
/// </summary>
Shadow = 1,
/// <summary>
/// Platform needs app-drawn window border/frame.
/// </summary>
Border = 2,
/// <summary>
/// Platform needs app-drawn resize grips.
/// </summary>
ResizeGrips = 4,
/// <summary>
/// Platform needs app-drawn window titlebar.
/// </summary>
TitleBar = 8,
}

37
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;
}
}

11
src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs

@ -1,11 +0,0 @@
using System.Linq;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
internal class ChromeOverlayLayer : Panel
{
}
}

38
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<ChromeOverlayLayer>() == null)
{
var layer = new ChromeOverlayLayer();
AddLayer(layer, ChromeZIndex);
layer.Children.Add(new TitleBar());
}
break;
}
parent = parent.VisualParent;
}
}
internal OverlayLayer? OverlayLayer
{
get

4
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);

252
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
{
/// <summary>
/// Wrapper that holds a single visual child, used to host decoration layer content
/// extracted from the decorations template.
/// </summary>
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;
/// <summary>
/// Gets the current drawn decorations instance, if active.
/// </summary>
internal WindowDrawnDecorations? Decorations => _decorations;
/// <summary>
/// Enables drawn window decorations with the specified parts.
/// Creates the decorations instance, applies the template, and inserts layers into the visual tree.
/// </summary>
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();
}
/// <summary>
/// Disables drawn window decorations and removes all layers.
/// </summary>
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();
}
/// <summary>
/// Shows or hides the fullscreen popover based on the window state.
/// Called by Window when window state changes.
/// </summary>
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();
}
}
}
}

98
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);
/// <summary>
/// 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.
/// </summary>
private class TopLevelHostAutomationPeer(TopLevelHost owner) : ControlAutomationPeer(owner)
{
protected override IReadOnlyList<AutomationPeer>? GetChildrenCore() => null;
}
private class DecorationsOverlaysAutomationPeer(TopLevelHost host, TopLevel topLevel) : AutomationPeer
{
private List<AutomationPeer> _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<AutomationPeer> 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;
}
}

14
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;
/// <summary>
/// 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.
/// </summary>
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);
}

147
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<bool> ExtendClientAreaToDecorationsHintProperty =
AvaloniaProperty.Register<Window, bool>(nameof(ExtendClientAreaToDecorationsHint), false);
public static readonly StyledProperty<ExtendClientAreaChromeHints> ExtendClientAreaChromeHintsProperty =
AvaloniaProperty.Register<Window, ExtendClientAreaChromeHints>(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default);
public static readonly StyledProperty<double> ExtendClientAreaTitleBarHeightHintProperty =
AvaloniaProperty.Register<Window, double>(nameof(ExtendClientAreaTitleBarHeightHint), -1);
@ -224,6 +222,7 @@ namespace Avalonia.Controls
static Window()
{
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler<Window>((w, _) => w.OnTitleBarHeightHintChanged());
}
/// <summary>
@ -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);
}
/// <summary>
/// Gets or Sets the <see cref="Avalonia.Platform.ExtendClientAreaChromeHints"/> that control
/// how the chrome looks when the client area is extended.
/// </summary>
public ExtendClientAreaChromeHints ExtendClientAreaChromeHints
{
get => GetValue(ExtendClientAreaChromeHintsProperty);
set => SetValue(ExtendClientAreaChromeHintsProperty, value);
}
/// <summary>
/// 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();
}
/// <summary>
/// Updates decoration parts based on current window state without
/// re-creating the decorations instance.
/// </summary>
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();
}
/// <summary>
/// Called by TopLevelHost when decoration effective geometry changes
/// (e.g. theme changes Default* values, or EnabledParts changes).
/// </summary>
internal void OnDrawnDecorationsGeometryChanged()
{
UpdateDrawnDecorationMargins();
}
/// <summary>
@ -771,6 +884,10 @@ namespace Avalonia.Controls
EnsureInitialized();
ApplyStyling();
// Enable drawn decorations before layout so margins are computed
UpdateDrawnDecorations();
_shown = true;
IsVisible = true;

5
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -92,6 +92,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action<bool> 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)
{
}

8
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<bool>? 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)
{
}

19
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<bool>? 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();
}
/// <inheritdoc/>
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
_native.SetExtendClientAreaHints ((AvnExtendClientAreaChromeHints)hints);
}
/// <inheritdoc/>
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();
}
/// <inheritdoc/>

10
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);

4
src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml

@ -821,7 +821,7 @@
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="Black" />
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
<!-- BaseResources for CaptionButtons.xaml -->
<!-- BaseResources for WindowDrawnDecorations.xaml -->
<SolidColorBrush x:Key="CaptionButtonForeground" Color="Black" />
<SolidColorBrush x:Key="CaptionButtonBackground" Color="#ffe5e5e5" />
<SolidColorBrush x:Key="CaptionButtonBorderBrush" Color="#ffcacaca" />
@ -1648,7 +1648,7 @@
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="White" />
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
<!-- BaseResources for CaptionButtons.xaml -->
<!-- BaseResources for WindowDrawnDecorations.xaml -->
<SolidColorBrush x:Key="CaptionButtonForeground" Color="White" />
<SolidColorBrush x:Key="CaptionButtonBackground" Color="#ffe5e5e5" />
<SolidColorBrush x:Key="CaptionButtonBorderBrush" Color="#ffcacaca" />

119
src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml

@ -1,119 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<x:Double x:Key="CaptionButtonWidth">45</x:Double>
<x:Double x:Key="CaptionButtonHeight">30</x:Double>
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">
<ThemeVariantScope RequestedThemeVariant="Dark">
<Border Background="Black">
<CaptionButtons Height="30"/>
</Border>
</ThemeVariantScope>
<ThemeVariantScope RequestedThemeVariant="Light">
<CaptionButtons Height="30"/>
</ThemeVariantScope>
</StackPanel>
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="FluentCaptionButton" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource CaptionButtonBackground}" />
<!-- Reusing BorderBrush to define pressed background color, as it's not used otherwise -->
<Setter Property="BorderBrush" Value="{DynamicResource CaptionButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CaptionButtonForeground}"/>
<Setter Property="Width" Value="{DynamicResource CaptionButtonWidth}"/>
<Setter Property="Height" Value="{DynamicResource CaptionButtonHeight}"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
Content="{TemplateBinding Content}"/>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding Background}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type CaptionButtons}" TargetType="CaptionButtons">
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel Spacing="2" VerticalAlignment="Stretch" TextElement.FontSize="10" Orientation="Horizontal">
<Button x:Name="PART_FullScreenButton"
Theme="{StaticResource FluentCaptionButton}"
IsVisible="False">
<Viewbox Width="11" Margin="2">
<Path Name="FullScreenButtonPath"
Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
Data="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" />
</Viewbox>
</Button>
<Button x:Name="PART_MinimizeButton"
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Minimize"
Win32Properties.NonClientHitTestResult="MinButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
Data="M2048 1229v-205h-2048v205h2048z" />
</Viewbox>
</Button>
<Button x:Name="PART_RestoreButton"
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Maximize"
Win32Properties.NonClientHitTestResult="MaxButton">
<Viewbox Width="11" Margin="2">
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Name="RestoreButtonPath"
Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
Data="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z"/>
</Viewbox>
</Button>
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Close"
Win32Properties.NonClientHitTestResult="Close">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Button>
</StackPanel>
</ControlTemplate>
</Setter>
<Style Selector="^:maximized /template/ Path#RestoreButtonPath">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<Style Selector="^:fullscreen /template/ Path#FullScreenButtonPath">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
<Style Selector="^:fullscreen /template/ Button#PART_RestoreButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Button:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
</ControlTheme>
</ResourceDictionary>

3
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -18,7 +18,6 @@
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CalendarItem.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Carousel.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/CheckBox.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml" />
@ -52,12 +51,12 @@
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ToggleButton.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ToolTip.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/TitleBar.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/TreeView.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Window.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ComboBox.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ContextMenu.xaml" />

62
src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml

@ -1,62 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<Design.PreviewWith>
<Border Height="30" Width="300">
<TitleBar Background="SkyBlue" Foreground="Black" />
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type TitleBar}" TargetType="TitleBar">
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Panel HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="Stretch">
<Panel x:Name="PART_MouseTracker"
Height="1"
VerticalAlignment="Top" />
<Panel x:Name="PART_Container">
<Border x:Name="PART_Background"
Background="{TemplateBinding Background}"
IsHitTestVisible="False"
Win32Properties.NonClientHitTestResult="Caption" />
<CaptionButtons x:Name="PART_CaptionButtons"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Foreground="{TemplateBinding Foreground}"
Win32Properties.NonClientHitTestResult="Client" />
</Panel>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:fullscreen">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="^:fullscreen /template/ Border#PART_Background">
<Setter Property="IsHitTestVisible" Value="True" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_MouseTracker">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="translateY(-30px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:.25" />
</Transitions>
</Setter>
</Style>
<Style Selector="^:fullscreen:pointerover /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="none" />
</Style>
</ControlTheme>
</ResourceDictionary>

215
src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml

@ -0,0 +1,215 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<x:Double x:Key="CaptionButtonWidth">45</x:Double>
<x:Double x:Key="CaptionButtonHeight">30</x:Double>
<ControlTheme x:Key="FluentDrawnCaptionButton" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource CaptionButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CaptionButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CaptionButtonForeground}"/>
<Setter Property="Width" Value="{DynamicResource CaptionButtonWidth}"/>
<Setter Property="Height" Value="{DynamicResource CaptionButtonHeight}"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="(WindowDecorationProperties.ElementRole)" Value="DecorationsElement"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
Content="{TemplateBinding Content}"/>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding Background}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type WindowDrawnDecorations}" TargetType="WindowDrawnDecorations">
<Setter Property="DefaultTitleBarHeight" Value="30"/>
<Setter Property="DefaultFrameThickness" Value="1"/>
<Setter Property="DefaultShadowThickness" Value="8"/>
<Setter Property="Template">
<WindowDrawnDecorationsTemplate>
<WindowDrawnDecorationsContent>
<WindowDrawnDecorationsContent.Underlay>
<Panel x:Name="PART_UnderlayWrapper">
<Border x:Name="PART_WindowBorder"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
BorderThickness="{TemplateBinding FrameThickness}"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}"
IsHitTestVisible="False" />
<!-- Titlebar: background, title text, and drag area live in underlay -->
<Panel x:Name="PART_TitleBar" VerticalAlignment="Top"
Height="{TemplateBinding TitleBarHeight}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
IsVisible="{TemplateBinding HasTitleBar}"
WindowDecorationProperties.ElementRole="TitleBar" />
</Panel>
</WindowDrawnDecorationsContent.Underlay>
<WindowDrawnDecorationsContent.Overlay>
<!-- Overlay: only interactive caption buttons -->
<Panel x:Name="PART_OverlayWrapper">
<!-- Title text lives in overlay so it renders above client content -->
<Panel x:Name="PART_TitleTextPanel" VerticalAlignment="Top"
Height="{TemplateBinding TitleBarHeight}"
IsHitTestVisible="False"
IsVisible="{TemplateBinding HasTitleBar}">
<TextBlock Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12" />
</Panel>
<StackPanel x:Name="PART_OverlayPanel"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Height="{TemplateBinding TitleBarHeight}"
IsVisible="{TemplateBinding HasTitleBar}"
Orientation="Horizontal"
Spacing="2"
TextElement.FontSize="10">
<Button x:Name="PART_FullScreenButton"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="FullScreenButton">
<Viewbox Width="11" Margin="2">
<Path Name="FullScreenButtonPath"
Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" />
</Viewbox>
</Button>
<Button x:Name="PART_MinimizeButton"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="MinimizeButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 1229v-205h-2048v205h2048z" />
</Viewbox>
</Button>
<Button x:Name="PART_MaximizeButton"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="MaximizeButton">
<Viewbox Width="11" Margin="2">
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Name="RestoreButtonPath"
Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z"/>
</Viewbox>
</Button>
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="CloseButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Button>
</StackPanel>
</Panel>
</WindowDrawnDecorationsContent.Overlay>
<WindowDrawnDecorationsContent.FullscreenPopover>
<Panel Height="{TemplateBinding DefaultTitleBarHeight}"
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
WindowDecorationProperties.ElementRole="TitleBar">
<TextBlock Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12" />
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="2"
TextElement.FontSize="10">
<Button x:Name="PART_PopoverFullScreenButton"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="FullScreenButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Viewbox>
</Button>
<Button x:Name="PART_PopoverCloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource FluentDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="CloseButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Button>
</StackPanel>
</Panel>
</WindowDrawnDecorationsContent.FullscreenPopover>
</WindowDrawnDecorationsContent>
</WindowDrawnDecorationsTemplate>
</Setter>
<!-- Shadow: inset border and add drop shadow -->
<Style Selector="^:has-shadow /template/ Panel#PART_UnderlayWrapper">
<Setter Property="Margin" Value="{TemplateBinding ShadowThickness}"/>
</Style>
<Style Selector="^:has-shadow /template/ Border#PART_WindowBorder">
<Setter Property="BoxShadow" Value="0 2 10 2 #80000000"/>
</Style>
<Style Selector="^:has-shadow /template/ Panel#PART_OverlayWrapper">
<Setter Property="Margin" Value="{TemplateBinding ShadowThickness}"/>
</Style>
<!-- Border: inset titlebar and buttons inside frame -->
<Style Selector="^:has-border /template/ Panel#PART_TitleTextPanel">
<Setter Property="Margin" Value="1,1,1,0"/>
</Style>
<Style Selector="^:has-border /template/ Panel#PART_TitleBar">
<Setter Property="Margin" Value="1,1,1,0"/>
</Style>
<Style Selector="^:has-border /template/ StackPanel#PART_OverlayPanel">
<Setter Property="Margin" Value="0,1,1,0"/>
</Style>
<!-- Maximized: restore button path changes -->
<Style Selector="^:maximized /template/ Path#RestoreButtonPath">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<!-- Fullscreen: update fullscreen button icon to "exit fullscreen" -->
<Style Selector="^:fullscreen /template/ Path#FullScreenButtonPath">
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
<!-- Disabled buttons -->
<Style Selector="^ /template/ Button:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ StackPanel#PART_OverlayPanel">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_TitleBar">
<Setter Property="IsVisible" Value="False" />
</Style>
</ControlTheme>
</ResourceDictionary>

4
src/Avalonia.Themes.Simple/Accents/Base.xaml

@ -42,7 +42,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
<!-- BaseResources for CaptionButtons.xaml -->
<!-- BaseResources for WindowDrawnDecorations.xaml -->
<SolidColorBrush x:Key="CaptionButtonForeground" Color="Black" />
<SolidColorBrush x:Key="CaptionButtonBackground" Color="#ffe5e5e5" />
<SolidColorBrush x:Key="CaptionButtonBorderBrush" Color="#ffcacaca" />
@ -87,7 +87,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
<!-- BaseResources for CaptionButtons.xaml -->
<!-- BaseResources for WindowDrawnDecorations.xaml -->
<SolidColorBrush x:Key="CaptionButtonForeground" Color="White" />
<SolidColorBrush x:Key="CaptionButtonBackground" Color="#ffe5e5e5" />
<SolidColorBrush x:Key="CaptionButtonBorderBrush" Color="#ffcacaca" />

126
src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml

@ -1,126 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">
<ThemeVariantScope RequestedThemeVariant="Dark">
<Border Background="Black">
<CaptionButtons Height="30"/>
</Border>
</ThemeVariantScope>
<ThemeVariantScope RequestedThemeVariant="Light">
<CaptionButtons Height="30"/>
</ThemeVariantScope>
</StackPanel>
</Border>
</Design.PreviewWith>
<x:Double x:Key="CaptionButtonWidth">45</x:Double>
<x:Double x:Key="CaptionButtonHeight">30</x:Double>
<ControlTheme x:Key="SimpleCaptionButton"
TargetType="Button">
<Setter Property="Background" Value="{DynamicResource CaptionButtonBackground}" />
<!-- Reusing BorderBrush to define pressed background color, as it's not used otherwise -->
<Setter Property="BorderBrush" Value="{DynamicResource CaptionButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CaptionButtonForeground}"/> <Setter Property="Width" Value="{DynamicResource CaptionButtonWidth}"/>
<Setter Property="Height" Value="{DynamicResource CaptionButtonHeight}"/>
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding Background}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type CaptionButtons}"
TargetType="CaptionButtons">
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel VerticalAlignment="Stretch"
Orientation="Horizontal"
Spacing="2"
TextElement.FontSize="10">
<Button x:Name="PART_FullScreenButton"
IsVisible="False"
Theme="{StaticResource SimpleCaptionButton}">
<Viewbox Width="11"
Margin="2">
<Path Name="FullScreenButtonPath"
Data="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z"
Fill="{TemplateBinding Foreground}"
Stretch="UniformToFill" />
</Viewbox>
</Button>
<Button x:Name="PART_MinimizeButton"
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Minimize"
Win32Properties.NonClientHitTestResult="MinButton">
<Viewbox Width="11"
Margin="2">
<Path Data="M2048 1229v-205h-2048v205h2048z"
Fill="{TemplateBinding Foreground}"
Stretch="UniformToFill" />
</Viewbox>
</Button>
<Button x:Name="PART_RestoreButton"
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Maximize"
Win32Properties.NonClientHitTestResult="MaxButton">
<Viewbox Width="11"
Margin="2">
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Name="RestoreButtonPath"
Data="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z"
Fill="{TemplateBinding Foreground}"
Stretch="UniformToFill" />
</Viewbox>
</Button>
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Close"
Win32Properties.NonClientHitTestResult="Close">
<Viewbox Width="11"
Margin="2">
<Path Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z"
Fill="{TemplateBinding Foreground}"
Stretch="UniformToFill" />
</Viewbox>
</Button>
</StackPanel>
</ControlTemplate>
</Setter>
<Style Selector="^:maximized /template/ Path#RestoreButtonPath">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<Style Selector="^:fullscreen /template/ Path#FullScreenButtonPath">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
<Style Selector="^:fullscreen /template/ Button#PART_RestoreButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Button#PART_RestoreButton:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
</ControlTheme>
</ResourceDictionary>

3
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@ -22,8 +22,7 @@
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ComboBox.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/Window.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/Carousel.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/CaptionButtons.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/TitleBar.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/TextBox.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml" />
<MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/DataValidationErrors.xaml" />

69
src/Avalonia.Themes.Simple/Controls/TitleBar.xaml

@ -1,69 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<Design.PreviewWith>
<Border>
<TitleBar Width="300"
Height="30"
Background="SkyBlue"
Foreground="Black" />
</Border>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type TitleBar}"
TargetType="TitleBar">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Panel HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="Stretch">
<Panel x:Name="PART_MouseTracker"
Height="1"
VerticalAlignment="Top" />
<Panel x:Name="PART_Container">
<Border x:Name="PART_Background"
Background="{TemplateBinding Background}"
IsHitTestVisible="False"
Win32Properties.NonClientHitTestResult="Caption" />
<CaptionButtons x:Name="PART_CaptionButtons"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Foreground="{TemplateBinding Foreground}"
Win32Properties.NonClientHitTestResult="Client" />
</Panel>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:fullscreen">
<Setter Property="Background" Value="{DynamicResource ThemeAccentColor}" />
</Style>
<Style Selector="^ /template/ Border#PART_Background">
<Setter Property="IsHitTestVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ Border#PART_Background">
<Setter Property="IsHitTestVisible" Value="True" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_MouseTracker">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="translateY(-30px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:.25" />
</Transitions>
</Setter>
</Style>
<Style Selector="^:fullscreen:pointerover /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="none" />
</Style>
</ControlTheme>
</ResourceDictionary>

221
src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml

@ -0,0 +1,221 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal">
<x:Double x:Key="CaptionButtonWidth">45</x:Double>
<x:Double x:Key="CaptionButtonHeight">30</x:Double>
<ControlTheme x:Key="SimpleDrawnCaptionButton" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource CaptionButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CaptionButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CaptionButtonForeground}"/>
<Setter Property="Width" Value="{DynamicResource CaptionButtonWidth}"/>
<Setter Property="Height" Value="{DynamicResource CaptionButtonHeight}"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="(WindowDecorationProperties.ElementRole)" Value="DecorationsElement"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
Content="{TemplateBinding Content}"/>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding Background}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{TemplateBinding BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type WindowDrawnDecorations}" TargetType="WindowDrawnDecorations">
<Setter Property="DefaultTitleBarHeight" Value="30"/>
<Setter Property="DefaultFrameThickness" Value="1"/>
<Setter Property="DefaultShadowThickness" Value="8"/>
<Setter Property="Template">
<WindowDrawnDecorationsTemplate>
<WindowDrawnDecorationsContent>
<WindowDrawnDecorationsContent.Underlay>
<Panel x:Name="PART_UnderlayWrapper">
<Border x:Name="PART_WindowBorder"
Background="{DynamicResource ThemeBackgroundBrush}"
BorderThickness="{TemplateBinding FrameThickness}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
IsHitTestVisible="False" />
<!-- Titlebar: background, title text, and drag area live in underlay -->
<Panel x:Name="PART_TitleBar" VerticalAlignment="Top"
Height="{TemplateBinding TitleBarHeight}"
Background="{DynamicResource ThemeBackgroundBrush}"
IsVisible="{TemplateBinding HasTitleBar}"
WindowDecorationProperties.ElementRole="TitleBar" />
</Panel>
</WindowDrawnDecorationsContent.Underlay>
<WindowDrawnDecorationsContent.Overlay>
<!-- Overlay: only interactive caption buttons -->
<Panel x:Name="PART_OverlayWrapper">
<!-- Title text lives in overlay so it renders above client content -->
<Panel x:Name="PART_TitleTextPanel" VerticalAlignment="Top"
Height="{TemplateBinding TitleBarHeight}"
IsHitTestVisible="False"
IsVisible="{TemplateBinding HasTitleBar}">
<TextBlock Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12" />
</Panel>
<StackPanel x:Name="PART_OverlayPanel"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Height="{TemplateBinding TitleBarHeight}"
Orientation="Horizontal"
Spacing="2"
IsVisible="{TemplateBinding HasTitleBar}"
TextElement.FontSize="10">
<Button x:Name="PART_FullScreenButton"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="FullScreenButton">
<Viewbox Width="11" Margin="2">
<Path Name="FullScreenButtonPath"
Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" />
</Viewbox>
</Button>
<Button x:Name="PART_MinimizeButton"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="MinimizeButton"
AutomationProperties.Name="Minimize">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 1229v-205h-2048v205h2048z" />
</Viewbox>
</Button>
<Button x:Name="PART_MaximizeButton"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="MaximizeButton"
AutomationProperties.Name="Maximize">
<Viewbox Width="11" Margin="2">
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Name="RestoreButtonPath"
Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z"/>
</Viewbox>
</Button>
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="CloseButton"
AutomationProperties.Name="Close">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Button>
</StackPanel>
</Panel>
</WindowDrawnDecorationsContent.Overlay>
<WindowDrawnDecorationsContent.FullscreenPopover>
<!-- Shown on hover at top edge in fullscreen mode -->
<Panel Height="30"
VerticalAlignment="Top"
Background="{DynamicResource ThemeAccentBrush}"
WindowDecorationProperties.ElementRole="TitleBar">
<TextBlock Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Foreground="White"
Margin="12,0,0,0"
FontSize="12" />
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="2"
TextElement.FontSize="10">
<Button x:Name="PART_PopoverFullScreenButton"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="FullScreenButton">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Viewbox>
</Button>
<Button x:Name="PART_PopoverCloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource SimpleDrawnCaptionButton}"
WindowDecorationProperties.ElementRole="CloseButton"
AutomationProperties.Name="Close">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{DynamicResource CaptionButtonForeground}"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Button>
</StackPanel>
</Panel>
</WindowDrawnDecorationsContent.FullscreenPopover>
</WindowDrawnDecorationsContent>
</WindowDrawnDecorationsTemplate>
</Setter>
<!-- Shadow: inset border and add drop shadow -->
<Style Selector="^:has-shadow /template/ Panel#PART_UnderlayWrapper">
<Setter Property="Margin" Value="{TemplateBinding ShadowThickness}"/>
</Style>
<Style Selector="^:has-shadow /template/ Border#PART_WindowBorder">
<Setter Property="BoxShadow" Value="0 2 10 2 #80000000"/>
</Style>
<Style Selector="^:has-shadow /template/ Panel#PART_OverlayWrapper">
<Setter Property="Margin" Value="{TemplateBinding ShadowThickness}"/>
</Style>
<!-- Border: inset titlebar and buttons inside frame -->
<Style Selector="^:has-border /template/ Panel#PART_TitleTextPanel">
<Setter Property="Margin" Value="1,1,1,0"/>
</Style>
<Style Selector="^:has-border /template/ Panel#PART_TitleBar">
<Setter Property="Margin" Value="1,1,1,0"/>
</Style>
<Style Selector="^:has-border /template/ StackPanel#PART_OverlayPanel">
<Setter Property="Margin" Value="0,1,1,0"/>
</Style>
<!-- Maximized: restore button path changes -->
<Style Selector="^:maximized /template/ Path#RestoreButtonPath">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<!-- Fullscreen: update fullscreen button icon to "exit fullscreen" -->
<Style Selector="^:fullscreen /template/ Path#FullScreenButtonPath">
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
<!-- Disabled buttons -->
<Style Selector="^ /template/ Button:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ StackPanel#PART_OverlayPanel">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:fullscreen /template/ Panel#PART_TitleBar">
<Setter Property="IsVisible" Value="False" />
</Style>
</ControlTheme>
</ResourceDictionary>

22
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
/// </summary>
public bool UseGLibMainLoop { get; set; }
/// <summary>
/// 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.
/// </summary>
[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; }
/// <summary>
/// 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,

83
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<PixelPoint>? 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;

7
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<bool>? 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)
{

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -49,6 +49,7 @@
<Compile Include="Templates\Template.cs" />
<Compile Include="Templates\TemplateContent.cs" />
<Compile Include="Templates\TreeDataTemplate.cs" />
<Compile Include="Templates\WindowDrawnDecorationsTemplate.cs" />
<Compile Include="XamlIl\Runtime\IAvaloniaXamlIlControlTemplateProvider.cs" />
<Compile Include="XamlIl\Runtime\IAvaloniaXamlIlParentStackProvider.cs" />
<Compile Include="XamlIl\Runtime\IAvaloniaXamlIlXmlNamespaceInfoProviderV1.cs" />

21
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<WindowDrawnDecorationsContent> Build() =>
TemplateContent.Load<WindowDrawnDecorationsContent>(Content)
?? throw new InvalidOperationException();
object? ITemplate.Build() => Build().Result;
}

32
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 =>

26
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();
}
/// <inheritdoc/>
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
@ -1620,7 +1610,12 @@ namespace Avalonia.Win32
public Action<bool>? ExtendClientAreaToDecorationsChanged { get; set; }
/// <inheritdoc/>
public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome);
public bool NeedsManagedDecorations => _isClientAreaExtended;
public PlatformRequestedDrawnDecoration RequestedDrawnDecorations =>
_isClientAreaExtended
? PlatformRequestedDrawnDecoration.TitleBar
: PlatformRequestedDrawnDecoration.None;
/// <inheritdoc/>
public Thickness ExtendedMargins => _extendedMargins;
@ -1731,3 +1726,4 @@ namespace Avalonia.Win32
}
}
}

12
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)

87
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();
}
}

25
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
}
}

5
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();
}
}

50
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();
}
}

10
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);

34
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<TitleBar>().FirstOrDefault();
Assert.NotNull(titleBar);
var buttons = titleBar.GetVisualDescendants().OfType<CaptionButtons>().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

Loading…
Cancel
Save