mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 14:39:42 +08:00
[GUI] add custom top-bar across al platforms (#682)
This commit is contained in:
@@ -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/Surfaces.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/Console.axaml" />
|
||||
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" />
|
||||
|
||||
@@ -14,9 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
WindowDecorations="Full"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
WindowDecorations="None"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||
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
|
||||
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">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
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}" />
|
||||
</Border>
|
||||
</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>
|
||||
|
||||
<!-- Main content -->
|
||||
@@ -662,5 +687,73 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
||||
</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>
|
||||
</Window>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
@@ -164,6 +165,22 @@ public partial class MainWindow : Window
|
||||
};
|
||||
|
||||
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.DoubleTapped += (_, _) => LaunchSelected();
|
||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||
@@ -813,12 +830,81 @@ public partial class MainWindow : Window
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 ----
|
||||
|
||||
private void ApplySettingsToControls()
|
||||
@@ -1831,6 +1917,8 @@ public partial class MainWindow : Window
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == WindowStateProperty)
|
||||
{
|
||||
UpdateWindowChromeState();
|
||||
|
||||
// The XAML WindowState="Maximized" assignment raises this change
|
||||
// during InitializeComponent, before named controls are wired up.
|
||||
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>
|
||||
Reference in New Issue
Block a user