feat: implement title bar and caption buttons.

This commit is contained in:
rabbitism
2024-06-26 21:04:50 +08:00
parent fd9a8668c5
commit 26873208a8
8 changed files with 379 additions and 37 deletions

View File

@@ -22,8 +22,7 @@
<Panel>
<Grid
Classes.Blur="{Binding #host.HasModal}"
ColumnDefinitions="Auto, *"
RowDefinitions="Auto, *">
ColumnDefinitions="Auto, *" >
<Grid.Styles>
<Style Selector="Grid.Blur">
<Setter Property="Effect" Value="blur(20)" />
@@ -31,7 +30,6 @@
</Grid.Styles>
<Border
Padding="8 4"
Grid.RowSpan="2"
VerticalAlignment="Stretch"
Theme="{DynamicResource CardBorder}">
<u:NavMenu Name="menu" ItemsSource="{Binding Menus.MenuItems}"
@@ -89,16 +87,9 @@
</u:NavMenu.Header>
</u:NavMenu>
</Border>
<u:ThemeToggleButton
Grid.Row="0"
Grid.Column="1"
IsThreeState="True"
Mode="Controller"
HorizontalAlignment="Right" />
<ContentControl
Grid.Row="1"
Grid.Column="1"
Margin="12"
Margin="12 36 12 12"
Content="{Binding Content}">
<ContentControl.ContentTemplate>
<converters:ViewLocator />

View File

@@ -11,8 +11,14 @@
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
IsFullScreenButtonVisible="True"
x:DataType="viewModels:MainWindowViewModel"
Icon="/Assets/Ursa.ico"
mc:Ignorable="d">
<u:UrsaWindow.RightContent>
<u:ThemeToggleButton
IsThreeState="True"
Mode="Controller" />
</u:UrsaWindow.RightContent>
<views:MainView />
</u:UrsaWindow>

View File

@@ -1,9 +1,11 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ursa.Controls;
namespace Ursa.Demo.Views;
public partial class MainWindow : Window
public partial class MainWindow : UrsaWindow
{
public MainWindow()
{

View File

@@ -1,8 +1,138 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:UrsaWindow}" TargetType="u:UrsaWindow">
<Setter Property="Background" Value="{DynamicResource WindowDefaultBackground}" />
<Setter Property="TransparencyBackgroundFallback" Value="{DynamicResource WindowDefaultBackground}" />
<Setter Property="Foreground" Value="{DynamicResource WindowDefaultForeground}" />
<Setter Property="FontSize" Value="{DynamicResource DefaultFontSize}" />
<Setter Property="FontFamily" Value="{DynamicResource DefaultFontFamily}" />
<Setter Property="ExtendClientAreaTitleBarHeightHint" Value="0" />
<Setter Property="ExtendClientAreaToDecorationsHint" Value="True" />
<Setter Property="SystemDecorations" Value="BorderOnly" />
<Setter Property="Template">
<ControlTemplate TargetType="u:UrsaWindow">
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}" IsHitTestVisible="False" />
<Panel Margin="{TemplateBinding WindowDecorationMargin}" Background="Transparent" />
<VisualLayerManager>
<VisualLayerManager.ChromeOverlayLayer>
<u:TitleBar
Content="{Binding $parent[u:UrsaWindow].TitleBarContent}"
Margin="{Binding $parent[u:UrsaWindow].TitleBarMargin}"
LeftContent="{Binding $parent[u:UrsaWindow].LeftContent}"
RightContent="{Binding $parent[u:UrsaWindow].RightContent}" />
</VisualLayerManager.ChromeOverlayLayer>
<Panel>
<ContentPresenter
Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Panel>
</VisualLayerManager>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:TitleBar}" TargetType="u:TitleBar">
<Setter Property="Background" Value="Transparent" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate TargetType="u:TitleBar">
<Panel Background="Transparent">
<Panel>
<Border
Name="PART_Background"
Background="{TemplateBinding Background}"
IsHitTestVisible="True" />
<Grid ColumnDefinitions="Auto, *, Auto, Auto" HorizontalAlignment="Stretch">
<ContentPresenter
Grid.Column="0"
VerticalAlignment="Center"
Content="{TemplateBinding LeftContent}" />
<ContentPresenter
Grid.Column="1"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
<ContentPresenter
Grid.Column="2"
VerticalAlignment="Center"
Content="{TemplateBinding RightContent}" />
<u:CaptionButtons
x:Name="PART_CaptionButtons"
Grid.Column="3"
Margin="8 0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
DockPanel.Dock="Right"
Foreground="{TemplateBinding Foreground}" />
</Grid>
</Panel>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:CaptionButtons}" TargetType="u:CaptionButtons">
<Setter Property="Template">
<ControlTemplate TargetType="u:CaptionButtons">
<StackPanel
VerticalAlignment="Stretch"
Orientation="Horizontal"
Spacing="2"
TextElement.FontSize="10">
<Button x:Name="PART_FullScreenButton" Theme="{DynamicResource CaptionButton}">
<PathIcon
Name="PART_FullScreenButtonIcon"
Width="12"
Height="12"
Data="{DynamicResource WindowExpandGlyph}"
Foreground="{Binding $parent[Button].Foreground}" />
</Button>
<Button x:Name="PART_MinimizeButton" Theme="{DynamicResource CaptionButton}">
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowMinimizeGlyph}"
Foreground="{Binding $parent[Button].Foreground}" />
</Button>
<Button x:Name="PART_RestoreButton" Theme="{DynamicResource CaptionButton}" >
<PathIcon
Name="PART_RestoreButtonIcon"
Width="12"
Height="12"
Data="{DynamicResource WindowMaximizeGlyph}"
Foreground="{Binding $parent[Button].Foreground}" />
</Button>
<Button
x:Name="PART_CloseButton"
Background="{DynamicResource CaptionButtonClosePointeroverBackground}"
BorderBrush="{DynamicResource CaptionButtonClosePressedBackground}"
Theme="{DynamicResource CaptionButton}">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button:pressed">
<Setter Property="Foreground" Value="White" />
</Style>
</Button.Styles>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}"
Foreground="{Binding $parent[Button].Foreground}" />
</Button>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -43,6 +43,7 @@
<ResourceInclude Source="TwoTonePathIcon.axaml" />
<ResourceInclude Source="ToolBar.axaml" />
<ResourceInclude Source="TimeBox.axaml"/>
<ResourceInclude Source="UrsaWindow.axaml"/>
<ResourceInclude Source="VerificationCode.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,75 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Irihi.Avalonia.Shared.Helpers;
using Irihi.Avalonia.Shared.Reactive;
namespace Ursa.Controls;
[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: Avalonia.Controls.Chrome.CaptionButtons
{
private const string PART_CloseButton = "PART_CloseButton";
private const string PART_RestoreButton = "PART_RestoreButton";
private const string PART_MinimizeButton = "PART_MinimizeButton";
private const string PART_FullScreenButton = "PART_FullScreenButton";
private Button? _closeButton;
private Button? _restoreButton;
private Button? _minimizeButton;
private Button? _fullScreenButton;
private IDisposable? _visibilityDisposable;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_closeButton = e.NameScope.Get<Button>(PART_CloseButton);
_restoreButton = e.NameScope.Get<Button>(PART_RestoreButton);
_minimizeButton = e.NameScope.Get<Button>(PART_MinimizeButton);
_fullScreenButton = e.NameScope.Get<Button>(PART_FullScreenButton);
Button.ClickEvent.AddHandler((o, args) => OnClose(), _closeButton);
Button.ClickEvent.AddHandler((o, args) => OnRestore(), _restoreButton);
Button.ClickEvent.AddHandler((o, args) => OnMinimize(), _minimizeButton);
Button.ClickEvent.AddHandler((o, args) => OnToggleFullScreen(), _fullScreenButton);
if (this.HostWindow is not null && !HostWindow.CanResize)
{
_restoreButton.IsEnabled = false;
}
UpdateVisibility();
}
public override void Attach(Window hostWindow)
{
base.Attach(hostWindow);
_visibilityDisposable = HostWindow?.GetObservable(Window.WindowStateProperty).Subscribe((a) =>
{
UpdateVisibility();
});
}
private void UpdateVisibility()
{
if (HostWindow is not UrsaWindow u)
{
return;
}
IsVisibleProperty.SetValue(u.IsCloseButtonVisible, _closeButton);
IsVisibleProperty.SetValue(u.WindowState != WindowState.FullScreen && u.IsRestoreButtonVisible,
_restoreButton);
IsVisibleProperty.SetValue(u.WindowState != WindowState.FullScreen && u.IsMinimizeButtonVisible,
_minimizeButton);
IsVisibleProperty.SetValue(u.IsFullScreenButtonVisible, _fullScreenButton);
}
public override void Detach()
{
base.Detach();
_visibilityDisposable?.Dispose();
}
}

View File

@@ -0,0 +1,91 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
public class TitleBar: ContentControl
{
private CaptionButtons? _captionButtons;
private InputElement? _background;
private Window? _visualRoot;
public static readonly StyledProperty<object?> LeftContentProperty = AvaloniaProperty.Register<TitleBar, object?>(
nameof(LeftContent));
public object? LeftContent
{
get => GetValue(LeftContentProperty);
set => SetValue(LeftContentProperty, value);
}
public static readonly StyledProperty<object?> RightContentProperty = AvaloniaProperty.Register<TitleBar, object?>(
nameof(RightContent));
public object? RightContent
{
get => GetValue(RightContentProperty);
set => SetValue(RightContentProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
this._captionButtons?.Detach();
this._captionButtons = e.NameScope.Get<CaptionButtons>("PART_CaptionButtons");
this._background = e.NameScope.Get<InputElement>("PART_Background");
if (!(this.VisualRoot is Window visualRoot))
return;
_visualRoot = visualRoot;
DoubleTappedEvent.AddHandler(OnDoubleTapped, _background);
PointerPressedEvent.AddHandler(OnPointerPressed, _background);
this._captionButtons?.Attach(visualRoot);
// this.UpdateSize(visualRoot);
}
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
_visualRoot?.BeginMoveDrag(e);
}
}
private void OnDoubleTapped(object sender, TappedEventArgs e)
{
if (_visualRoot is not null)
{
if (_visualRoot.WindowState == WindowState.Maximized)
{
_visualRoot.WindowState = WindowState.Normal;
}
else
{
_visualRoot.WindowState = WindowState.Maximized;
}
}
}
private void UpdateSize(Window window)
{
Thickness offScreenMargin = window.OffScreenMargin;
var left = offScreenMargin.Left;
offScreenMargin = window.OffScreenMargin;
double top = offScreenMargin.Top;
offScreenMargin = window.OffScreenMargin;
double right = offScreenMargin.Right;
offScreenMargin = window.OffScreenMargin;
double bottom = offScreenMargin.Bottom;
this.Margin = new Thickness(left, top, right, bottom);
if (window.WindowState != WindowState.FullScreen)
{
this.Height = window.WindowDecorationMargin.Top;
if (this._captionButtons != null)
this._captionButtons.Height = this.Height;
}
}
}

View File

@@ -9,42 +9,88 @@ namespace Ursa.Controls;
/// </summary>
public class UrsaWindow: Window
{
public static readonly StyledProperty<bool> ShowFullScreenButtonProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(ShowFullScreenButton), false);
protected override Type StyleKeyOverride => typeof(UrsaWindow);
public bool ShowFullScreenButton
public static readonly StyledProperty<bool> IsFullScreenButtonVisibleProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsFullScreenButtonVisible));
public bool IsFullScreenButtonVisible
{
get => GetValue(ShowFullScreenButtonProperty);
set => SetValue(ShowFullScreenButtonProperty, value);
get => GetValue(IsFullScreenButtonVisibleProperty);
set => SetValue(IsFullScreenButtonVisibleProperty, value);
}
public static readonly StyledProperty<bool> ShowMaximumButtonProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(ShowMaximumButton));
public static readonly StyledProperty<bool> IsMinimizeButtonVisibleProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsMinimizeButtonVisible), true);
public bool ShowMaximumButton
public bool IsMinimizeButtonVisible
{
get => GetValue(ShowMaximumButtonProperty);
set => SetValue(ShowMaximumButtonProperty, value);
get => GetValue(IsMinimizeButtonVisibleProperty);
set => SetValue(IsMinimizeButtonVisibleProperty, value);
}
public static readonly StyledProperty<bool> ShowMinimumButtonProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(ShowMinimumButton));
public static readonly StyledProperty<bool> IsRestoreButtonVisibleProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsRestoreButtonVisible), true);
public bool ShowMinimumButton
public bool IsRestoreButtonVisible
{
get => GetValue(ShowMinimumButtonProperty);
set => SetValue(ShowMinimumButtonProperty, value);
get => GetValue(IsRestoreButtonVisibleProperty);
set => SetValue(IsRestoreButtonVisibleProperty, value);
}
public static readonly StyledProperty<bool> ShowCloseButtonProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(ShowCloseButton));
public static readonly StyledProperty<bool> IsCloseButtonVisibleProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsCloseButtonVisible), true);
public bool ShowCloseButton
public bool IsCloseButtonVisible
{
get => GetValue(ShowCloseButtonProperty);
set => SetValue(ShowCloseButtonProperty, value);
get => GetValue(IsCloseButtonVisibleProperty);
set => SetValue(IsCloseButtonVisibleProperty, value);
}
public static readonly StyledProperty<bool> IsTitleBarVisibleProperty = AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsTitleBarVisible));
public bool IsTitleBarVisible
{
get => GetValue(IsTitleBarVisibleProperty);
set => SetValue(IsTitleBarVisibleProperty, value);
}
public static readonly StyledProperty<object?> TitleBarContentProperty = AvaloniaProperty.Register<UrsaWindow, object?>(
nameof(TitleBarContent));
public object? TitleBarContent
{
get => GetValue(TitleBarContentProperty);
set => SetValue(TitleBarContentProperty, value);
}
public static readonly StyledProperty<object?> LeftContentProperty = AvaloniaProperty.Register<UrsaWindow, object?>(
nameof(LeftContent));
public object? LeftContent
{
get => GetValue(LeftContentProperty);
set => SetValue(LeftContentProperty, value);
}
public static readonly StyledProperty<object?> RightContentProperty = AvaloniaProperty.Register<UrsaWindow, object?>(
nameof(RightContent));
public object? RightContent
{
get => GetValue(RightContentProperty);
set => SetValue(RightContentProperty, value);
}
public static readonly StyledProperty<Thickness> TitleBarMarginProperty = AvaloniaProperty.Register<UrsaWindow, Thickness>(
nameof(TitleBarMargin));
public Thickness TitleBarMargin
{
get => GetValue(TitleBarMarginProperty);
set => SetValue(TitleBarMarginProperty, value);
}
}