From 11ec00205d90c9fd8f70b5bf8131c61fa13e8ac3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 19 Apr 2026 12:29:03 +0100 Subject: [PATCH] Add Window.WindowDecorationsTheme property (#21061) --- .../TopLevelHost.Decorations.cs | 13 ++++- src/Avalonia.Controls/Window.cs | 25 ++++++++- .../WindowTests.cs | 52 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TopLevelHost.Decorations.cs b/src/Avalonia.Controls/TopLevelHost.Decorations.cs index 11f3dc3ada..a13cb4ecab 100644 --- a/src/Avalonia.Controls/TopLevelHost.Decorations.cs +++ b/src/Avalonia.Controls/TopLevelHost.Decorations.cs @@ -6,6 +6,7 @@ using Avalonia.Automation.Peers; using Avalonia.Controls.Chrome; using Avalonia.Input; using Avalonia.Reactive; +using Avalonia.Styling; namespace Avalonia.Controls; @@ -53,7 +54,7 @@ internal partial class TopLevelHost /// When non-null (including ), the decoration /// infrastructure is kept alive and parts/fullscreen state are updated. /// - internal void UpdateDrawnDecorations(DrawnWindowDecorationParts? parts, WindowState windowState) + internal void UpdateDrawnDecorations(DrawnWindowDecorationParts? parts, WindowState windowState, ControlTheme? theme) { if (parts == null) { @@ -68,6 +69,14 @@ internal partial class TopLevelHost // Layers persist across part changes; pseudo-classes driven by EnabledParts // control visibility of individual decoration elements in the theme. _decorations.EnabledParts = enabledParts; + + var oldTheme = _decorations.Theme; + if (oldTheme != theme) + { + _decorations.Theme = theme; + _decorations.ApplyStyling(); + } + if (_resizeGrips != null) _resizeGrips.IsVisible = enabledParts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); } @@ -75,6 +84,7 @@ internal partial class TopLevelHost { _decorations = new WindowDrawnDecorations(); _decorations.EnabledParts = enabledParts; + _decorations.Theme = theme; // Set up logical parenting LogicalChildren.Add(_decorations); @@ -161,6 +171,7 @@ internal partial class TopLevelHost if (_decorations == null) return; + _decorations.ApplyStyling(); _decorations.ApplyTemplate(); var content = _decorations.Content; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 0797f39733..37b75b1b27 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -140,6 +140,12 @@ namespace Avalonia.Controls public static readonly StyledProperty WindowDecorationsProperty = AvaloniaProperty.Register(nameof(WindowDecorations), WindowDecorations.Full); + /// + /// Defines the property. + /// + public static readonly StyledProperty WindowDecorationsThemeProperty = + AvaloniaProperty.Register(nameof(WindowDecorationsTheme)); + /// /// Defines the property. /// @@ -376,6 +382,15 @@ namespace Avalonia.Controls set => SetValue(WindowDecorationsProperty, value); } + /// + /// Gets or sets the theme used to render the window decorations when they are not drawn by the system. + /// + public ControlTheme? WindowDecorationsTheme + { + get => GetValue(WindowDecorationsThemeProperty); + set => SetValue(WindowDecorationsThemeProperty, value); + } + [Obsolete("Use WindowDecorations instead.")] public WindowDecorations SystemDecorations { @@ -704,7 +719,7 @@ namespace Avalonia.Controls // Detect forced mode: platform needs managed decorations but app hasn't opted in _isForcedDecorationMode = parts != null && !IsExtendedIntoWindowDecorations; - TopLevelHost.UpdateDrawnDecorations(parts, WindowState); + TopLevelHost.UpdateDrawnDecorations(parts, WindowState, WindowDecorationsTheme); if (parts != null) { @@ -739,7 +754,7 @@ namespace Avalonia.Controls if (TopLevelHost.Decorations == null) return; - TopLevelHost.UpdateDrawnDecorations(ComputeDecorationParts(), WindowState); + TopLevelHost.UpdateDrawnDecorations(ComputeDecorationParts(), WindowState, WindowDecorationsTheme); } private Chrome.DrawnWindowDecorationParts? ComputeDecorationParts() @@ -1493,6 +1508,7 @@ namespace Avalonia.Controls protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); + if (change.Property == WindowDecorationsProperty) { var (_, typedNewValue) = change.GetOldAndNewValue(); @@ -1500,6 +1516,11 @@ namespace Avalonia.Controls PlatformImpl?.SetWindowDecorations(typedNewValue); } + else if (change.Property == WindowDecorationsThemeProperty) + { + UpdateDrawnDecorations(); + } + else if (change.Property == OwnerProperty) { var oldParent = change.OldValue as Window; diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 3b2386b1d5..646dad30ec 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -2,9 +2,13 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; +using Avalonia.Controls.Templates; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -1590,6 +1594,54 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void WindowDecorationsTheme_Should_Apply_To_Decorations() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.NeedsManagedDecorations).Returns(true); + windowImpl.Setup(x => x.RequestedDrawnDecorations).Returns( + PlatformRequestedDrawnDecoration.TitleBar | PlatformRequestedDrawnDecoration.Border); + + var window = new Window(windowImpl.Object); + + var (theme1, content1) = CreateTheme(); + window.WindowDecorationsTheme = theme1; + window.Show(); + + var decorations = window.TopLevelHost.Decorations; + Assert.NotNull(decorations); + Assert.Same(theme1, decorations.Theme); + Assert.Same(content1, decorations.Content); + + var (theme2, content2) = CreateTheme(); + window.WindowDecorationsTheme = theme2; + + Assert.Same(theme2, decorations.Theme); + Assert.Same(content2, decorations.Content); + + static (ControlTheme theme, WindowDrawnDecorationsContent content) CreateTheme() + { + var content = new WindowDrawnDecorationsContent(); + + var template = new WindowDrawnDecorationsTemplate + { + Content = (IServiceProvider? _) => new TemplateResult(content, new NameScope()) + }; + + var theme = new ControlTheme(typeof(WindowDrawnDecorations)) + { + Setters = + { + new Setter(WindowDrawnDecorations.TemplateProperty, template) + } + }; + + return (theme, content); + } + } + private class TopmostWindow : Window { static TopmostWindow()