Browse Source

Implement WindowDrawnDecorations.TitleBarDecorations (#21969)

* Implement WindowDrawnDecorations.TitleBarDecorations

* Add TitleBarDecorations sample to ControlCatalog
pull/22015/head
Julien Lebosquain 1 month ago
committed by GitHub
parent
commit
13f272d928
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      samples/ControlCatalog/MainWindow.xaml
  2. 59
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  3. 2
      samples/ControlCatalog/ScrollPage.xaml
  4. 52
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  5. 45
      src/Avalonia.Controls/Chrome/TitleBarDecorations.cs
  6. 59
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  7. 18
      src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
  8. 18
      src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml
  9. 92
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

1
samples/ControlCatalog/MainWindow.xaml

@ -12,6 +12,7 @@
CanResize="{Binding CanResize}"
CanMinimize="{Binding CanMinimize}"
CanMaximize="{Binding CanMaximize}"
WindowDrawnDecorations.TitleBarDecorations="{Binding TitleBarDecorations}"
Win32Properties.WindowCornerPreference="{Binding Win32WindowCornerPreference}"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"
x:DataType="vm:MainWindowViewModel">

59
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -9,28 +9,58 @@
x:DataType="viewModels:MainWindowViewModel"
x:CompileBindings="True"
DataContext="{Binding $parent[Window].DataContext}">
<StackPanel>
<StackPanel Spacing="20">
<StackPanel
Spacing="10"
Margin="25"
IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2"
<TextBlock Classes="h1"
Text="Desktop properties"
Margin="4" />
<CheckBox Content="Extend Client Area to Decorations"
IsChecked="{Binding ExtendClientAreaEnabled}" />
<DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
<StackPanel IsVisible="{Binding ExtendClientAreaEnabled}"
Margin="28,0,0,0">
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
Margin="8,-10" />
<Grid ColumnDefinitions="Auto,*,Auto">
</DockPanel>
<TextBlock Text="Height:"
VerticalAlignment="Center"
Grid.Column="0" />
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
TickFrequency="1"
IsSnapToTickEnabled="True"
Margin="8,-10"
Grid.Column="1" />
<TextBlock Text="{Binding TitleBarHeight, StringFormat=F0}"
MinWidth="50"
VerticalAlignment="Center"
Grid.Column="2" />
</Grid>
<CheckBox Content="Title"
IsChecked="{Binding ShowTitle}" />
<CheckBox Content="Full Screen Button"
IsChecked="{Binding ShowFullScreenButton}" />
<CheckBox Content="Minimize Button"
IsChecked="{Binding ShowMinimizeButton}" />
<CheckBox Content="Maximize Button"
IsChecked="{Binding ShowMaximizeButton}" />
<CheckBox Content="Close Button"
IsChecked="{Binding ShowCloseButton}" />
</StackPanel>
<CheckBox Content="Can Resize"
IsChecked="{Binding CanResize}" />
@ -45,7 +75,7 @@
<DockPanel>
<Label Content="Corner Preference (Windows 11+): "
Margin="4"
Margin="0,4"
DockPanel.Dock="Left" />
<ComboBox ItemsSource="{Binding Win32WindowCornerPreferences}"
@ -57,11 +87,12 @@
</StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
<StackPanel IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h1" Text="Mobile properties" Margin="4" />
<CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
<CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}"
Margin="0,4" />
</StackPanel>
</StackPanel>

2
samples/ControlCatalog/ScrollPage.xaml

@ -15,7 +15,7 @@
HorizontalContentAlignment="Stretch"
IsVisible="False" />
<ScrollViewer Margin="20,20,5,0"
<ScrollViewer Padding="20"
HorizontalScrollBarVisibility="Disabled">
<ContentPresenter Name="PART_ContentPresenter"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"

52
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,8 +1,10 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Chrome;
using Avalonia.Dialogs;
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using Avalonia;
using MiniMvvm;
@ -122,6 +124,56 @@ namespace ControlCatalog.ViewModels
set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); }
}
public TitleBarDecorations TitleBarDecorations
{
get;
set { RaiseAndSetIfChanged(ref field, value); }
} = TitleBarDecorations.All;
public bool ShowTitle
{
get => HasTitleBarDecoration(TitleBarDecorations.Title);
set => SetTitleBarDecoration(TitleBarDecorations.Title, value);
}
public bool ShowFullScreenButton
{
get => HasTitleBarDecoration(TitleBarDecorations.FullScreenButton);
set => SetTitleBarDecoration(TitleBarDecorations.FullScreenButton, value);
}
public bool ShowMinimizeButton
{
get => HasTitleBarDecoration(TitleBarDecorations.MinimizeButton);
set => SetTitleBarDecoration(TitleBarDecorations.MinimizeButton, value);
}
public bool ShowMaximizeButton
{
get => HasTitleBarDecoration(TitleBarDecorations.MaximizeButton);
set => SetTitleBarDecoration(TitleBarDecorations.MaximizeButton, value);
}
public bool ShowCloseButton
{
get => HasTitleBarDecoration(TitleBarDecorations.CloseButton);
set => SetTitleBarDecoration(TitleBarDecorations.CloseButton, value);
}
private bool HasTitleBarDecoration(TitleBarDecorations decoration)
=> (TitleBarDecorations & decoration) != 0;
private void SetTitleBarDecoration(TitleBarDecorations decoration, bool value, [CallerMemberName] string? propertyName = null)
{
var newDecorations = value ? TitleBarDecorations | decoration : TitleBarDecorations & ~decoration;
if (newDecorations == TitleBarDecorations)
return;
TitleBarDecorations = newDecorations;
RaisePropertyChanged(propertyName);
RaisePropertyChanged(nameof(TitleBarDecorations));
}
public MiniCommand AboutCommand { get; }
public MiniCommand ExitCommand { get; }

45
src/Avalonia.Controls/Chrome/TitleBarDecorations.cs

@ -0,0 +1,45 @@
using System;
namespace Avalonia.Controls.Chrome;
/// <summary>
/// Flags specifying which elements are displayed in a drawn window title bar of <see cref="WindowDrawnDecorations"/>.
/// </summary>
[Flags]
public enum TitleBarDecorations
{
/// <summary>
/// No title bar element is displayed.
/// </summary>
None = 0,
/// <summary>
/// The window title is displayed.
/// </summary>
Title = 1 << 0,
/// <summary>
/// The minimize button is displayed.
/// </summary>
MinimizeButton = 1 << 1,
/// <summary>
/// The maximize button is displayed.
/// </summary>
MaximizeButton = 1 << 2,
/// <summary>
/// The close button is displayed.
/// </summary>
CloseButton = 1 << 3,
/// <summary>
/// The full screen button is displayed.
/// </summary>
FullScreenButton = 1 << 4,
/// <summary>
/// All title bar elements are displayed.
/// </summary>
All = Title | MinimizeButton | MaximizeButton | CloseButton | FullScreenButton
}

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

@ -17,7 +17,7 @@ namespace Avalonia.Controls.Chrome;
/// and inserts them into its own visual tree.
/// </summary>
[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar,
pcHasMaximize, pcHasFullscreen, pcHasMinimize)]
pcHasMaximize, pcHasFullscreen, pcHasMinimize, pcHasClose, pcHasTitle)]
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_MinimizeButton, typeof(Button))]
[TemplatePart(PART_MaximizeButton, typeof(Button))]
@ -36,6 +36,8 @@ public class WindowDrawnDecorations : StyledElement
internal const string pcHasMaximize = ":has-maximize";
internal const string pcHasFullscreen = ":has-fullscreen";
internal const string pcHasMinimize = ":has-minimize";
internal const string pcHasClose = ":has-close";
internal const string pcHasTitle = ":has-title";
// Template part names for caption buttons
internal const string PART_CloseButton = "PART_CloseButton";
@ -126,6 +128,13 @@ public class WindowDrawnDecorations : StyledElement
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<WindowDrawnDecorations, string?>(nameof(Title));
/// <summary>
/// Defines the TitleBarDecorations attached property.
/// </summary>
public static readonly AttachedProperty<TitleBarDecorations> TitleBarDecorationsProperty =
AvaloniaProperty.RegisterAttached<WindowDrawnDecorations, StyledElement, TitleBarDecorations>(
"TitleBarDecorations", TitleBarDecorations.All);
/// <summary>
/// Defines the <see cref="EnabledParts"/> property.
/// </summary>
@ -244,6 +253,7 @@ public class WindowDrawnDecorations : StyledElement
/// <summary>
/// Gets or sets the window title displayed in the decorations.
/// Mirrors the value set on the host <see cref="Window"/>.
/// </summary>
public string? Title
{
@ -251,6 +261,31 @@ public class WindowDrawnDecorations : StyledElement
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets which title bar elements to display.
/// </summary>
public static TitleBarDecorations GetTitleBarDecorations(StyledElement element)
=> element.GetValue(TitleBarDecorationsProperty);
/// <summary>
/// Sets which title bar elements to display.
/// </summary>
/// <remarks>
/// Set this property on a <see cref="Window"/> to control the elements displayed in its drawn title bar.
/// </remarks>
public static void SetTitleBarDecorations(StyledElement element, TitleBarDecorations value)
=> element.SetValue(TitleBarDecorationsProperty, value);
/// <summary>
/// Gets or sets which title bar elements are requested to be displayed.
/// Mirrors the value set on the host <see cref="Window"/>.
/// </summary>
public TitleBarDecorations TitleBarDecorations
{
get => GetValue(TitleBarDecorationsProperty);
set => SetValue(TitleBarDecorationsProperty, value);
}
/// <summary>
/// Gets or sets which decoration parts are enabled.
/// Set by Window based on platform capabilities and user preferences.
@ -334,6 +369,7 @@ public class WindowDrawnDecorations : StyledElement
x.UpdateEffectiveGeometry();
});
TitleBarDecorationsProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateTitleBarDecorationsPseudoClasses());
DefaultTitleBarHeightProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
DefaultFrameThicknessProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
DefaultShadowThicknessProperty.Changed.AddClassHandler<WindowDrawnDecorations>((x, _) => x.UpdateEffectiveGeometry());
@ -395,6 +431,8 @@ public class WindowDrawnDecorations : StyledElement
Disposable.Create(() => window.AllowedWindowActionsChanged -= OnAllowedWindowActionsChanged),
window.GetObservable(Window.TitleProperty)
.Subscribe(title => SetCurrentValue(TitleProperty, title)),
window.GetObservable(TitleBarDecorationsProperty)
.Subscribe(hints => SetCurrentValue(TitleBarDecorationsProperty, hints)),
window.GetObservable(Window.CanMaximizeProperty)
.Subscribe(_ =>
{
@ -415,7 +453,7 @@ public class WindowDrawnDecorations : StyledElement
}),
};
UpdateAllowedActionsPseudoClasses();
UpdateTitleBarDecorationsPseudoClasses();
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
@ -592,18 +630,25 @@ public class WindowDrawnDecorations : StyledElement
private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions)
{
UpdateAllowedActionsPseudoClasses();
UpdateTitleBarDecorationsPseudoClasses();
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}
private void UpdateAllowedActionsPseudoClasses()
private void UpdateTitleBarDecorationsPseudoClasses()
{
var actions = EffectiveAllowedActions;
PseudoClasses.Set(pcHasMaximize, actions.HasFlag(PlatformAllowedWindowActions.Maximize));
PseudoClasses.Set(pcHasFullscreen, actions.HasFlag(PlatformAllowedWindowActions.Fullscreen));
PseudoClasses.Set(pcHasMinimize, actions.HasFlag(PlatformAllowedWindowActions.Minimize));
var hints = TitleBarDecorations;
PseudoClasses.Set(pcHasMaximize, actions.HasFlag(PlatformAllowedWindowActions.Maximize)
&& hints.HasFlag(TitleBarDecorations.MaximizeButton));
PseudoClasses.Set(pcHasFullscreen, actions.HasFlag(PlatformAllowedWindowActions.Fullscreen)
&& hints.HasFlag(TitleBarDecorations.FullScreenButton));
PseudoClasses.Set(pcHasMinimize, actions.HasFlag(PlatformAllowedWindowActions.Minimize)
&& hints.HasFlag(TitleBarDecorations.MinimizeButton));
PseudoClasses.Set(pcHasClose, hints.HasFlag(TitleBarDecorations.CloseButton));
PseudoClasses.Set(pcHasTitle, hints.HasFlag(TitleBarDecorations.Title));
}
private void UpdateEffectiveGeometry()

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

@ -128,7 +128,8 @@
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
WindowDecorationProperties.ElementRole="TitleBar">
<TextBlock Text="{TemplateBinding Title}"
<TextBlock x:Name="PART_PopoverTitleText"
Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12" />
@ -196,7 +197,8 @@
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Hide caption buttons when the platform does not support the action -->
<!-- Hide title bar elements when the platform does not support the action
or when they aren't requested by WindowDrawnDecorations.TitleBarDecorationHints -->
<Style Selector="^:not(:has-minimize) /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
@ -209,6 +211,18 @@
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_PopoverFullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-close) /template/ Button#PART_CloseButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-close) /template/ Button#PART_PopoverCloseButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-title) /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-title) /template/ TextBlock#PART_PopoverTitleText">
<Setter Property="IsVisible" Value="False"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">

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

@ -131,7 +131,8 @@
VerticalAlignment="Top"
Background="{DynamicResource ThemeAccentBrush}"
WindowDecorationProperties.ElementRole="TitleBar">
<TextBlock Text="{TemplateBinding Title}"
<TextBlock x:Name="PART_PopoverTitleText"
Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Foreground="White"
Margin="12,0,0,0"
@ -202,7 +203,8 @@
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Hide caption buttons when the platform does not support the action -->
<!-- Hide title bar elements when the platform does not support the action
or when they aren't requested by WindowDrawnDecorations.TitleBarDecorationHints -->
<Style Selector="^:not(:has-minimize) /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
@ -215,6 +217,18 @@
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_PopoverFullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-close) /template/ Button#PART_CloseButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-close) /template/ Button#PART_PopoverCloseButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-title) /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-title) /template/ TextBlock#PART_PopoverTitleText">
<Setter Property="IsVisible" Value="False"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">

92
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -1642,6 +1642,98 @@ namespace Avalonia.Controls.UnitTests
}
}
public class TitleBarDecorationsTests : ScopedTestBase
{
private static Window CreateWindowWithDrawnDecorations(PlatformAllowedWindowActions allowedActions = PlatformAllowedWindowActions.All)
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.NeedsManagedDecorations).Returns(true);
windowImpl.Setup(x => x.RequestedDrawnDecorations).Returns(
PlatformRequestedDrawnDecoration.TitleBar | PlatformRequestedDrawnDecoration.Border);
windowImpl.Setup(x => x.AllowedWindowActions).Returns(allowedActions);
return new Window(windowImpl.Object);
}
private static void AssertClassDecorations(WindowDrawnDecorations drawnDecorations, TitleBarDecorations expected)
{
AssertClassDecoration(TitleBarDecorations.Title, ":has-title");
AssertClassDecoration(TitleBarDecorations.MinimizeButton, ":has-minimize");
AssertClassDecoration(TitleBarDecorations.MaximizeButton, ":has-maximize");
AssertClassDecoration(TitleBarDecorations.CloseButton, ":has-close");
AssertClassDecoration(TitleBarDecorations.FullScreenButton, ":has-fullscreen");
void AssertClassDecoration(TitleBarDecorations decoration, string className)
=> Assert.Equal(expected.HasFlag(decoration), drawnDecorations.Classes.Contains(className));
}
[Fact]
public void All_Decorations_Should_Be_Visible_By_Default()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = CreateWindowWithDrawnDecorations();
window.Show();
var decorations = window.TopLevelHost.Decorations;
Assert.NotNull(decorations);
Assert.Equal(TitleBarDecorations.All, decorations.TitleBarDecorations);
AssertClassDecorations(decorations, TitleBarDecorations.All);
}
[Fact]
public void Decorations_Set_Before_Show_Should_Apply_Correct_Classes()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
const TitleBarDecorations decorations = TitleBarDecorations.Title | TitleBarDecorations.CloseButton;
var window = CreateWindowWithDrawnDecorations();
WindowDrawnDecorations.SetTitleBarDecorations(window, decorations);
window.Show();
var drawnDecorations = window.TopLevelHost.Decorations;
Assert.NotNull(drawnDecorations);
Assert.Equal(decorations, drawnDecorations.TitleBarDecorations);
AssertClassDecorations(drawnDecorations, decorations);
}
[Fact]
public void Decorations_Set_After_Show_Should_Apply_Correct_Classes()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = CreateWindowWithDrawnDecorations();
window.Show();
var drawnDecorations = window.TopLevelHost.Decorations;
Assert.NotNull(drawnDecorations);
WindowDrawnDecorations.SetTitleBarDecorations(window, TitleBarDecorations.None);
AssertClassDecorations(drawnDecorations, TitleBarDecorations.None);
WindowDrawnDecorations.SetTitleBarDecorations(window, TitleBarDecorations.MinimizeButton);
AssertClassDecorations(drawnDecorations, TitleBarDecorations.MinimizeButton);
}
[Fact]
public void Decorations_Should_Not_Show_Buttons_Unsupported_By_The_Platform()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = CreateWindowWithDrawnDecorations(PlatformAllowedWindowActions.Minimize);
window.Show();
var decorations = window.TopLevelHost.Decorations;
Assert.NotNull(decorations);
// Maximize and fullscreen are requested, but not allowed by the platform.
AssertClassDecorations(
decorations,
TitleBarDecorations.Title | TitleBarDecorations.MinimizeButton | TitleBarDecorations.CloseButton);
}
}
private class TopmostWindow : Window
{
static TopmostWindow()

Loading…
Cancel
Save