[GUI] add custom top-bar across al platforms (#682)

This commit is contained in:
Daniel Freak
2026-07-28 21:24:43 +03:00
committed by GitHub
parent b21dd9fb92
commit 882a6c06e0
4 changed files with 217 additions and 5 deletions
+1
View File
@@ -25,6 +25,7 @@ cascade order so individual launcher views do not redefine global visuals.
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Base.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Base.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Surfaces.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Surfaces.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Buttons.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Buttons.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Chrome.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" />
+97 -4
View File
@@ -14,9 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
WindowState="Maximized" WindowState="Maximized"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}" Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True" WindowDecorations="None"
WindowDecorations="Full"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown"> KeyDown="OnKeyDown">
@@ -46,7 +44,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so <!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
the game gets the whole screen. --> the game gets the whole screen. -->
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}"> <Grid x:Name="TitleBar"
Grid.Row="0"
Height="44"
ColumnDefinitions="*,Auto"
Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20" <Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" /> RenderOptions.BitmapInterpolationMode="HighQuality" />
@@ -55,6 +57,29 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" /> <TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border> </Border>
</StackPanel> </StackPanel>
<StackPanel x:Name="WindowChromeButtons"
Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button x:Name="MinimizeButton"
Classes="windowChrome"
ToolTip.Tip="Minimize"
AutomationProperties.Name="Minimize window">
<TextBlock Text="—" FontSize="16" />
</Button>
<Button x:Name="MaximizeButton"
Classes="windowChrome"
ToolTip.Tip="Restore"
AutomationProperties.Name="Restore window">
<TextBlock x:Name="MaximizeGlyph" Text="❐" FontSize="13" />
</Button>
<Button x:Name="CloseButton"
Classes="windowChrome windowClose"
ToolTip.Tip="Close"
AutomationProperties.Name="Close window">
<TextBlock Text="×" FontSize="20" />
</Button>
</StackPanel>
</Grid> </Grid>
<!-- Main content --> <!-- Main content -->
@@ -662,5 +687,73 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11" <TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" /> Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
</Grid> </Grid>
<!-- Portable resize hit targets for the frameless desktop window. They
are disabled while maximized or fullscreen in code-behind. -->
<Panel x:Name="ResizeHandles"
Grid.RowSpan="3"
ZIndex="1000"
IsVisible="False">
<Border Tag="North"
Height="6"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="#01000000"
Cursor="TopSide"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="South"
Height="6"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Background="#01000000"
Cursor="BottomSide"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="West"
Width="6"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="#01000000"
Cursor="LeftSide"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="East"
Width="6"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Background="#01000000"
Cursor="RightSide"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="NorthWest"
Width="12"
Height="12"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="#01000000"
Cursor="TopLeftCorner"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="NorthEast"
Width="12"
Height="12"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="#01000000"
Cursor="TopRightCorner"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="SouthWest"
Width="12"
Height="12"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Background="#01000000"
Cursor="BottomLeftCorner"
PointerPressed="OnResizeHandlePointerPressed" />
<Border Tag="SouthEast"
Width="12"
Height="12"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Background="#01000000"
Cursor="BottomRightCorner"
PointerPressed="OnResizeHandlePointerPressed" />
</Panel>
</Grid> </Grid>
</Window> </Window>
+89 -1
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia; using Avalonia;
using Avalonia.Automation;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
@@ -164,6 +165,22 @@ public partial class MainWindow : Window
}; };
TitleBar.PointerPressed += OnTitleBarPointerPressed; TitleBar.PointerPressed += OnTitleBarPointerPressed;
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
MinimizeButton.Click += (_, _) => WindowState = WindowState.Minimized;
MaximizeButton.Click += (_, _) => ToggleMaximized();
CloseButton.Click += (_, _) => Close();
Opened += (_, _) =>
{
// Some compositors ignore the initial maximized state while a
// frameless native window is still being created.
if (WindowState == WindowState.Normal)
{
WindowState = WindowState.Maximized;
}
UpdateWindowChromeState();
};
UpdateWindowChromeState();
GameList.SelectionChanged += (_, _) => UpdateSelectedGame(); GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
GameList.DoubleTapped += (_, _) => LaunchSelected(); GameList.DoubleTapped += (_, _) => LaunchSelected();
SearchBox.TextChanged += (_, _) => RefreshVisibleGames(); SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
@@ -813,12 +830,81 @@ public partial class MainWindow : Window
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.Source is Visual source &&
source.FindAncestorOfType<Button>(includeSelf: true) is null &&
e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
BeginMoveDrag(e); BeginMoveDrag(e);
} }
} }
private void OnTitleBarDoubleTapped(object? sender, TappedEventArgs e)
{
if (e.Source is Visual source &&
source.FindAncestorOfType<Button>(includeSelf: true) is null)
{
ToggleMaximized();
e.Handled = true;
}
}
private void ToggleMaximized()
{
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
}
private void UpdateMaximizeButton()
{
if (MaximizeGlyph is not { } glyph || MaximizeButton is not { } button)
{
return;
}
var isMaximized = WindowState == WindowState.Maximized;
glyph.Text = isMaximized ? "❐" : "□";
ToolTip.SetTip(button, isMaximized ? "Restore" : "Maximize");
AutomationProperties.SetName(
button,
isMaximized ? "Restore window" : "Maximize window");
}
private void UpdateWindowChromeState()
{
UpdateMaximizeButton();
var isFullscreen = WindowState == WindowState.FullScreen;
if (TitleBar is { } titleBar)
{
titleBar.IsVisible = !isFullscreen;
}
if (StatusBar is { } statusBar)
{
statusBar.IsVisible = !isFullscreen;
}
if (ResizeHandles is { } handles)
{
handles.IsVisible = CanResize && WindowState == WindowState.Normal;
}
}
private void OnResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
{
if (WindowState != WindowState.Normal ||
!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Control { Tag: string edgeName } ||
!Enum.TryParse<WindowEdge>(edgeName, out var edge))
{
return;
}
BeginResizeDrag(edge, e);
e.Handled = true;
}
// ---- Settings ---- // ---- Settings ----
private void ApplySettingsToControls() private void ApplySettingsToControls()
@@ -1831,6 +1917,8 @@ public partial class MainWindow : Window
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == WindowStateProperty) if (change.Property == WindowStateProperty)
{ {
UpdateWindowChromeState();
// The XAML WindowState="Maximized" assignment raises this change // The XAML WindowState="Maximized" assignment raises this change
// during InitializeComponent, before named controls are wired up. // during InitializeComponent, before named controls are wired up.
if (WindowState == WindowState.Minimized) if (WindowState == WindowState.Minimized)
@@ -0,0 +1,30 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
Window chrome button sizing and interaction states.
-->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="Button.windowChrome">
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="44" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.windowChrome:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.windowClose:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
</Styles>