Compare commits
5 Commits
1016_part3
...
add_demo_g
| Author | SHA1 | Date |
|---|---|---|
|
|
6bb75a4049 | |
|
|
0af8dce081 | |
|
|
289f090b50 | |
|
|
47d4c07a8d | |
|
|
1811cef944 |
|
|
@ -9,6 +9,7 @@
|
|||
[Aa]rtifacts/
|
||||
logs/
|
||||
.tmp/
|
||||
src/SharpEmu.Demo/
|
||||
|
||||
x86/
|
||||
x64/
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.5-nexus-release.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
|
|
|||
|
|
@ -180,4 +180,71 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<SharpEmuDemoTag>e3a6dd3</SharpEmuDemoTag>
|
||||
<SharpEmuDemoFolderName>demo</SharpEmuDemoFolderName>
|
||||
<SharpEmuDemoCacheDir>$(BaseIntermediateOutputPath)sharpemu-demo/$(SharpEmuDemoTag)</SharpEmuDemoCacheDir>
|
||||
<SharpEmuDemoArchive>$(SharpEmuDemoCacheDir)/sharpemu-demo.zip</SharpEmuDemoArchive>
|
||||
<SharpEmuDemoDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '..', 'SharpEmu.Demo'))</SharpEmuDemoDir>
|
||||
<SharpEmuDemoStamp>$(SharpEmuDemoDir).tag</SharpEmuDemoStamp>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="FetchSharpEmuDemo" BeforeTargets="Publish;Build">
|
||||
<ReadLinesFromFile File="$(SharpEmuDemoStamp)" Condition="Exists('$(SharpEmuDemoStamp)')">
|
||||
<Output TaskParameter="Lines" PropertyName="_SharpEmuDemoExtractedTag" />
|
||||
</ReadLinesFromFile>
|
||||
<PropertyGroup>
|
||||
<_SharpEmuDemoCurrent Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoExtractedTag)' == '$(SharpEmuDemoTag)'">true</_SharpEmuDemoCurrent>
|
||||
</PropertyGroup>
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/sharpemu-demo/releases/download/$(SharpEmuDemoTag)/sharpemu-demo.zip"
|
||||
DestinationFolder="$(SharpEmuDemoCacheDir)"
|
||||
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And !Exists('$(SharpEmuDemoArchive)')"
|
||||
ContinueOnError="WarnAndContinue" />
|
||||
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(_SharpEmuDemoCurrent)' != 'true'" />
|
||||
<Unzip
|
||||
SourceFiles="$(SharpEmuDemoArchive)"
|
||||
DestinationFolder="$(SharpEmuDemoDir)"
|
||||
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And Exists('$(SharpEmuDemoArchive)')"
|
||||
ContinueOnError="WarnAndContinue" />
|
||||
<PropertyGroup>
|
||||
<SharpEmuDemoComplete Condition="Exists('$(SharpEmuDemoDir)eboot.bin') And Exists('$(SharpEmuDemoDir)sce_sys/param.json') And Exists('$(SharpEmuDemoDir)sce_sys/icon0.png') And Exists('$(SharpEmuDemoDir)sce_sys/pic0.png')">true</SharpEmuDemoComplete>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile
|
||||
File="$(SharpEmuDemoStamp)"
|
||||
Lines="$(SharpEmuDemoTag)"
|
||||
Overwrite="true"
|
||||
Condition="'$(SharpEmuDemoComplete)' == 'true' And '$(_SharpEmuDemoCurrent)' != 'true'" />
|
||||
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(SharpEmuDemoComplete)' != 'true'" />
|
||||
<Delete Files="$(SharpEmuDemoArchive)" Condition="'$(SharpEmuDemoComplete)' != 'true' And Exists('$(SharpEmuDemoArchive)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildSharpEmuDemo" AfterTargets="Build" DependsOnTargets="FetchSharpEmuDemo">
|
||||
<ItemGroup>
|
||||
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
|
||||
</ItemGroup>
|
||||
<RemoveDir
|
||||
Directories="$(OutDir)$(SharpEmuDemoFolderName)"
|
||||
Condition="'$(SharpEmuDemoComplete)' != 'true'"
|
||||
ContinueOnError="WarnAndContinue" />
|
||||
<Copy
|
||||
SourceFiles="@(_SharpEmuDemoFiles)"
|
||||
DestinationFiles="@(_SharpEmuDemoFiles->'$(OutDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishSharpEmuDemo" AfterTargets="Publish" DependsOnTargets="FetchSharpEmuDemo">
|
||||
<ItemGroup>
|
||||
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
|
||||
</ItemGroup>
|
||||
<RemoveDir
|
||||
Directories="$(PublishDir)$(SharpEmuDemoFolderName)"
|
||||
Condition="'$(SharpEmuDemoComplete)' != 'true'"
|
||||
ContinueOnError="WarnAndContinue" />
|
||||
<Copy
|
||||
SourceFiles="@(_SharpEmuDemoFiles)"
|
||||
DestinationFiles="@(_SharpEmuDemoFiles->'$(PublishDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
internal static class BundledDemo
|
||||
{
|
||||
internal const string FolderName = "demo";
|
||||
|
||||
private static readonly string[] RequiredFiles =
|
||||
[
|
||||
"eboot.bin",
|
||||
Path.Combine("sce_sys", "param.json"),
|
||||
Path.Combine("sce_sys", "icon0.png"),
|
||||
Path.Combine("sce_sys", "pic0.png"),
|
||||
Path.Combine("sce_sys", "snd0.at9"),
|
||||
];
|
||||
|
||||
internal static string? FindEboot(string baseDirectory)
|
||||
{
|
||||
var root = Path.Combine(baseDirectory, FolderName);
|
||||
foreach (var file in RequiredFiles)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(root, file)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(root, "eboot.bin"));
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ namespace SharpEmu.GUI;
|
|||
public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, INotifyCollectionChanged
|
||||
{
|
||||
private readonly ObservableCollection<GameEntry> _games;
|
||||
private bool _includeAddFolderTile = true;
|
||||
|
||||
public LibraryTileCollection(ObservableCollection<GameEntry> games)
|
||||
{
|
||||
|
|
@ -24,7 +25,22 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
|
|||
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
|
||||
public int Count => _games.Count + 1;
|
||||
public bool IncludeAddFolderTile
|
||||
{
|
||||
get => _includeAddFolderTile;
|
||||
set
|
||||
{
|
||||
if (_includeAddFolderTile == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_includeAddFolderTile = value;
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => _games.Count + (_includeAddFolderTile ? 1 : 0);
|
||||
|
||||
bool IList.IsFixedSize => false;
|
||||
|
||||
|
|
@ -43,7 +59,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
|
|||
return _games[index];
|
||||
}
|
||||
|
||||
if (index == _games.Count)
|
||||
if (_includeAddFolderTile && index == _games.Count)
|
||||
{
|
||||
return AddFolderTile.Instance;
|
||||
}
|
||||
|
|
@ -65,7 +81,10 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
|
|||
yield return game;
|
||||
}
|
||||
|
||||
yield return AddFolderTile.Instance;
|
||||
if (_includeAddFolderTile)
|
||||
{
|
||||
yield return AddFolderTile.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
|
@ -79,7 +98,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
|
|||
int IList.IndexOf(object? value) => value switch
|
||||
{
|
||||
GameEntry game => _games.IndexOf(game),
|
||||
AddFolderTile => _games.Count,
|
||||
AddFolderTile when _includeAddFolderTile => _games.Count,
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
<Button x:Name="CarouselAddFolderButton"
|
||||
Classes="ghost"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="False"
|
||||
AutomationProperties.Name="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="+"
|
||||
FontSize="16"
|
||||
FontWeight="Light"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
<!-- Selected-game identity and actions share one detail surface so
|
||||
|
|
|
|||
|
|
@ -178,6 +178,19 @@ public partial class MainWindow : Window
|
|||
private GameEntry? _lastSelectedGame;
|
||||
private double _libraryRailRowHeight = 188;
|
||||
private double _libraryGridRowHeight = 216;
|
||||
private const double CarouselLeadingPadding = 20;
|
||||
private const double CarouselItemMargin = 12;
|
||||
private bool _carouselAnimating;
|
||||
private readonly TranslateTransform _carouselTransform = new();
|
||||
private double _carouselOffset;
|
||||
private double _carouselScrollFrom;
|
||||
private double _carouselScrollTo;
|
||||
private long _carouselScrollStartedAt;
|
||||
private bool _carouselPointerDown;
|
||||
private bool _carouselDragging;
|
||||
private Point _carouselDragStart;
|
||||
private double _carouselDragStartOffset;
|
||||
private double _carouselWheelAccumulator;
|
||||
|
||||
// Bundled key art shown whenever no game-specific backdrop applies; the
|
||||
// plain window color remains the fallback when the asset fails to load.
|
||||
|
|
@ -246,6 +259,27 @@ public partial class MainWindow : Window
|
|||
_libraryLayoutTimer.Stop();
|
||||
UpdateLibraryLayoutMetrics();
|
||||
};
|
||||
CarouselAddFolderButton.Click += (_, _) => StartAddFolderFromTile();
|
||||
GameList.AddHandler(PointerPressedEvent, OnCarouselPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
GameList.AddHandler(PointerMovedEvent, OnCarouselPointerMoved, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
GameList.AddHandler(PointerReleasedEvent, OnCarouselPointerReleased, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
GameList.AddHandler(
|
||||
PointerCaptureLostEvent,
|
||||
(_, e) =>
|
||||
{
|
||||
if (ReferenceEquals(e.Source, GameList))
|
||||
{
|
||||
EndCarouselDrag(select: false);
|
||||
}
|
||||
},
|
||||
RoutingStrategies.Tunnel | RoutingStrategies.Bubble,
|
||||
handledEventsToo: true);
|
||||
LibraryPage.AddHandler(PointerWheelChangedEvent, OnCarouselPointerWheel, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
GameList.SizeChanged += (_, _) =>
|
||||
{
|
||||
UpdateCarouselPadding();
|
||||
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: false), DispatcherPriority.Loaded);
|
||||
};
|
||||
|
||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
|
||||
|
|
@ -376,13 +410,7 @@ public partial class MainWindow : Window
|
|||
Closing += (_, _) => BeginWindowClosing();
|
||||
Closed += (_, _) => CompleteWindowClosing();
|
||||
|
||||
SdlLauncherGamepad.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
};
|
||||
EnvRenderDocToggle.IsCheckedChanged += (_, _) =>
|
||||
|
||||
SetEnvironmentToggle(
|
||||
"SHARPEMU_RENDERDOC",
|
||||
EnvRenderDocToggle.IsChecked == true);
|
||||
|
|
@ -390,24 +418,6 @@ public partial class MainWindow : Window
|
|||
SetEnvironmentToggle(
|
||||
"SHARPEMU_VK_DISABLE_IMPLICITS",
|
||||
EnvDisableVkImplicitLayersToggle.IsChecked == true);
|
||||
DefaultProfileBox.TextChanged += (_, _) =>
|
||||
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
|
||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||
|
||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
|
||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
||||
CtxCopyPath.Click += async (_, _) =>
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
|
||||
CtxCopyTitleId.Click += async (_, _) =>
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
|
||||
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
|
||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
Closing += (_, _) => BeginWindowClosing();
|
||||
Closed += (_, _) => CompleteWindowClosing();
|
||||
|
||||
SdlLauncherGamepad.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
|
|
@ -492,12 +502,25 @@ public partial class MainWindow : Window
|
|||
() => SetOptionsNavigationIndicator(_optionsSectionIndex, animate: false),
|
||||
DispatcherPriority.Loaded);
|
||||
}
|
||||
else
|
||||
{
|
||||
FocusLibrarySelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLibraryLayout(bool grid)
|
||||
{
|
||||
_isLibraryGridLayout = grid;
|
||||
var selectedTile = GameList.SelectedItem;
|
||||
_libraryTiles.IncludeAddFolderTile = grid;
|
||||
if (selectedTile is GameEntry && !ReferenceEquals(GameList.SelectedItem, selectedTile))
|
||||
{
|
||||
GameList.SelectedItem = selectedTile;
|
||||
}
|
||||
|
||||
CarouselAddFolderButton.IsVisible = !grid;
|
||||
SetClass(GameList, "gridLayout", grid);
|
||||
UpdateCarouselPadding();
|
||||
SetClass(LibrarySelectedDetails, "gridLayout", grid);
|
||||
LibraryPage.Margin = new Thickness(0, 6, 0, 0);
|
||||
UpdateLibraryLayoutMetrics();
|
||||
|
|
@ -506,7 +529,11 @@ public partial class MainWindow : Window
|
|||
if (GameList.SelectedItem is { } selected)
|
||||
{
|
||||
Dispatcher.UIThread.Post(
|
||||
() => GameList.ScrollIntoView(selected),
|
||||
() =>
|
||||
{
|
||||
GameList.ScrollIntoView(selected);
|
||||
AnchorCarouselSelection(animate: false);
|
||||
},
|
||||
DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
|
@ -518,8 +545,17 @@ public partial class MainWindow : Window
|
|||
Resources["LibraryTileWidth"] = metrics.ItemWidth;
|
||||
Resources["LibraryRailItemHeight"] = metrics.RailItemHeight;
|
||||
Resources["LibraryGridItemHeight"] = metrics.GridItemHeight;
|
||||
_libraryRailRowHeight = metrics.RailItemHeight + 16;
|
||||
_libraryRailRowHeight = metrics.RailItemHeight + 16 + 48;
|
||||
CarouselAddFolderButton.Margin = new Thickness(0, metrics.RailItemHeight + 24, 0, 0);
|
||||
_libraryGridRowHeight = metrics.GridItemHeight + 16;
|
||||
UpdateCarouselPadding();
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
UpdateCarouselPadding();
|
||||
AnchorCarouselSelection(animate: false);
|
||||
},
|
||||
DispatcherPriority.Loaded);
|
||||
|
||||
GameList.MaxHeight = double.PositiveInfinity;
|
||||
UpdateLibraryMinimumHeight();
|
||||
|
|
@ -850,11 +886,28 @@ public partial class MainWindow : Window
|
|||
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
||||
{
|
||||
MoveSelection(-1);
|
||||
ReturnFocusFromCarouselAddFolder();
|
||||
}
|
||||
|
||||
if (ShouldNavigate(right, ref _navRightNextAt, now))
|
||||
{
|
||||
MoveSelection(1);
|
||||
ReturnFocusFromCarouselAddFolder();
|
||||
}
|
||||
|
||||
if (!_isLibraryGridLayout)
|
||||
{
|
||||
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
|
||||
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
|
||||
if (ShouldNavigate(down, ref _navDownNextAt, now))
|
||||
{
|
||||
CarouselAddFolderButton.Focus(NavigationMethod.Directional);
|
||||
}
|
||||
|
||||
if (ShouldNavigate(up, ref _navUpNextAt, now))
|
||||
{
|
||||
ReturnFocusFromCarouselAddFolder();
|
||||
}
|
||||
}
|
||||
|
||||
if (_isLibraryGridLayout)
|
||||
|
|
@ -877,7 +930,14 @@ public partial class MainWindow : Window
|
|||
var pressed = pad.Buttons & ~_previousPadButtons;
|
||||
if ((pressed & HostGamepadButtons.Cross) != 0)
|
||||
{
|
||||
LaunchSelected();
|
||||
if (!_isLibraryGridLayout && CarouselAddFolderButton.IsFocused)
|
||||
{
|
||||
StartAddFolderFromTile();
|
||||
}
|
||||
else
|
||||
{
|
||||
LaunchSelected();
|
||||
}
|
||||
}
|
||||
|
||||
_previousPadButtons = pad.Buttons;
|
||||
|
|
@ -940,6 +1000,10 @@ public partial class MainWindow : Window
|
|||
: Math.Clamp(GameList.SelectedIndex + delta, 0, _libraryTiles.Count - 1);
|
||||
GameList.SelectedIndex = index;
|
||||
GameList.ScrollIntoView(index);
|
||||
if (GameList.IsKeyboardFocusWithin && GameList.ContainerFromIndex(index) is { } container)
|
||||
{
|
||||
container.Focus(NavigationMethod.Unspecified);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnOpenedAsync()
|
||||
|
|
@ -953,6 +1017,7 @@ public partial class MainWindow : Window
|
|||
: $" · UNOFFICIAL {BuildInfo.CommitSha}";
|
||||
VersionText.Text = display;
|
||||
Title = $"SharpEmu {display}";
|
||||
ReportGuiRenderer();
|
||||
ToolTip.SetTip(VersionText, BuildInfo.Banner);
|
||||
|
||||
_settings = GuiSettings.Load();
|
||||
|
|
@ -970,7 +1035,66 @@ public partial class MainWindow : Window
|
|||
}
|
||||
|
||||
SeedLibraryFromCache();
|
||||
FocusLibrarySelection();
|
||||
await RescanLibraryAsync();
|
||||
FocusLibrarySelection();
|
||||
}
|
||||
|
||||
private void ReturnFocusFromCarouselAddFolder()
|
||||
{
|
||||
if (CarouselAddFolderButton.IsFocused)
|
||||
{
|
||||
FocusLibrarySelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportGuiRenderer()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var angle = Process.GetCurrentProcess().Modules
|
||||
.Cast<ProcessModule>()
|
||||
.Any(static module => string.Equals(module.ModuleName, "av_libglesv2.dll", StringComparison.OrdinalIgnoreCase));
|
||||
AppendConsoleLine(
|
||||
angle ? "[GUI] Renderer: GPU (ANGLE/Direct3D)" : "[GUI] Renderer: software (ANGLE not loaded)",
|
||||
angle ? DimLineBrush : WarningLineBrush);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void FocusLibrarySelection()
|
||||
{
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
if (_activePageIndex != 0 || _isGameSettingsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameList.SelectedIndex < 0 && _visibleGames.Count > 0)
|
||||
{
|
||||
GameList.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
AnchorCarouselSelection(animate: false);
|
||||
if (GameList.SelectedIndex >= 0 && GameList.ContainerFromIndex(GameList.SelectedIndex) is { } container)
|
||||
{
|
||||
container.Focus(NavigationMethod.Unspecified);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameList.Focus(NavigationMethod.Unspecified);
|
||||
}
|
||||
},
|
||||
DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void PopulateLanguageBox()
|
||||
|
|
@ -1884,21 +2008,7 @@ public partial class MainWindow : Window
|
|||
continue;
|
||||
}
|
||||
|
||||
long size = 0;
|
||||
try
|
||||
{
|
||||
size = new FileInfo(fullPath).Length;
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
|
||||
}
|
||||
|
||||
var (title, titleId, version) = TryReadParamJson(fullPath);
|
||||
games.Add(new GameEntry(
|
||||
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
|
||||
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
||||
games.Add(CreateGameEntry(fullPath));
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
@ -1909,9 +2019,41 @@ public partial class MainWindow : Window
|
|||
}
|
||||
|
||||
games.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
|
||||
AddBundledDemo(games, excludedPaths);
|
||||
return games;
|
||||
}
|
||||
|
||||
private static void AddBundledDemo(List<GameEntry> games, IReadOnlySet<string> excludedPaths)
|
||||
{
|
||||
var demo = BundledDemo.FindEboot(AppContext.BaseDirectory);
|
||||
if (demo is null || excludedPaths.Contains(demo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
games.RemoveAll(game => string.Equals(game.Path, demo, GameLibraryPath.Comparison));
|
||||
games.Insert(0, CreateGameEntry(demo));
|
||||
}
|
||||
|
||||
private static GameEntry CreateGameEntry(string fullPath)
|
||||
{
|
||||
long size = 0;
|
||||
try
|
||||
{
|
||||
size = new FileInfo(fullPath).Length;
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
|
||||
}
|
||||
|
||||
var (title, titleId, version) = TryReadParamJson(fullPath);
|
||||
return new GameEntry(
|
||||
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
|
||||
FindCoverFor(fullPath), FindBackgroundFor(fullPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the game title, title id and content version from
|
||||
/// sce_sys/param.json next to the executable, when present.
|
||||
|
|
@ -2083,6 +2225,195 @@ public partial class MainWindow : Window
|
|||
|
||||
_lastSelectedGame = GameList.SelectedItem as GameEntry;
|
||||
UpdateSelectedGame();
|
||||
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: true), DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private double CarouselItemStride() =>
|
||||
(Resources.TryGetValue("LibraryTileWidth", out var width) && width is double tileWidth ? tileWidth : 0) +
|
||||
CarouselItemMargin;
|
||||
|
||||
private void UpdateCarouselPadding()
|
||||
{
|
||||
if (GameList.ItemsPanelRoot is not { } panel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isLibraryGridLayout)
|
||||
{
|
||||
panel.ClearValue(MarginProperty);
|
||||
if (ReferenceEquals(panel.RenderTransform, _carouselTransform))
|
||||
{
|
||||
panel.RenderTransform = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
panel.Margin = new Thickness(CarouselLeadingPadding, 0, 0, 0);
|
||||
if (!ReferenceEquals(panel.RenderTransform, _carouselTransform))
|
||||
{
|
||||
panel.RenderTransform = _carouselTransform;
|
||||
}
|
||||
|
||||
SetCarouselOffset(_carouselOffset);
|
||||
}
|
||||
|
||||
private double MaximumCarouselOffset() =>
|
||||
Math.Max(0, (_visibleGames.Count - 1) * CarouselItemStride());
|
||||
|
||||
private void SetCarouselOffset(double offset)
|
||||
{
|
||||
_carouselOffset = Math.Clamp(offset, 0, MaximumCarouselOffset());
|
||||
_carouselTransform.X = -_carouselOffset;
|
||||
}
|
||||
|
||||
private void AnchorCarouselSelection(bool animate)
|
||||
{
|
||||
if (_isLibraryGridLayout || GameList.SelectedIndex < 0 || _carouselPointerDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = Math.Clamp(GameList.SelectedIndex * CarouselItemStride(), 0, MaximumCarouselOffset());
|
||||
if (!animate)
|
||||
{
|
||||
_carouselAnimating = false;
|
||||
SetCarouselOffset(target);
|
||||
return;
|
||||
}
|
||||
|
||||
_carouselScrollFrom = _carouselOffset;
|
||||
_carouselScrollTo = target;
|
||||
_carouselScrollStartedAt = Stopwatch.GetTimestamp();
|
||||
if (!_carouselAnimating)
|
||||
{
|
||||
_carouselAnimating = true;
|
||||
RequestAnimationFrame(OnCarouselAnimationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCarouselPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (_isLibraryGridLayout || !e.GetCurrentPoint(GameList).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_carouselPointerDown = true;
|
||||
_carouselDragging = false;
|
||||
_carouselDragStart = e.GetPosition(GameList);
|
||||
_carouselDragStartOffset = _carouselOffset;
|
||||
}
|
||||
|
||||
private void OnCarouselPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_carouselPointerDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deltaX = e.GetPosition(GameList).X - _carouselDragStart.X;
|
||||
if (!_carouselDragging)
|
||||
{
|
||||
if (Math.Abs(deltaX) < 8)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_carouselDragging = true;
|
||||
_carouselAnimating = false;
|
||||
_carouselDragStartOffset = _carouselOffset;
|
||||
_carouselDragStart = e.GetPosition(GameList);
|
||||
deltaX = 0;
|
||||
e.Pointer.Capture(GameList);
|
||||
}
|
||||
|
||||
SetCarouselOffset(_carouselDragStartOffset - deltaX);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCarouselPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_carouselPointerDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dragged = _carouselDragging;
|
||||
EndCarouselDrag(select: true);
|
||||
if (dragged)
|
||||
{
|
||||
e.Pointer.Capture(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void EndCarouselDrag(bool select)
|
||||
{
|
||||
var dragged = _carouselDragging;
|
||||
_carouselPointerDown = false;
|
||||
_carouselDragging = false;
|
||||
if (!dragged || !select || _visibleGames.Count == 0)
|
||||
{
|
||||
if (!dragged)
|
||||
{
|
||||
AnchorCarouselSelection(animate: true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var stride = CarouselItemStride();
|
||||
var index = stride <= 0 ? 0 : (int)Math.Round(_carouselOffset / stride);
|
||||
index = Math.Clamp(index, 0, _visibleGames.Count - 1);
|
||||
if (GameList.SelectedIndex == index)
|
||||
{
|
||||
AnchorCarouselSelection(animate: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameList.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCarouselPointerWheel(object? sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (_isLibraryGridLayout || _isGameSettingsOpen || _activePageIndex != 0 || _visibleGames.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y) ? -e.Delta.X : e.Delta.Y;
|
||||
_carouselWheelAccumulator += delta;
|
||||
while (Math.Abs(_carouselWheelAccumulator) >= 1)
|
||||
{
|
||||
var step = _carouselWheelAccumulator > 0 ? -1 : 1;
|
||||
_carouselWheelAccumulator += step;
|
||||
MoveSelection(step);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCarouselAnimationFrame(TimeSpan frameTime)
|
||||
{
|
||||
if (!_carouselAnimating || _carouselDragging)
|
||||
{
|
||||
_carouselAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = Math.Min(1.0, Stopwatch.GetElapsedTime(_carouselScrollStartedAt).TotalMilliseconds / 260.0);
|
||||
var eased = 1 - Math.Pow(1 - progress, 3);
|
||||
SetCarouselOffset(_carouselScrollFrom + (_carouselScrollTo - _carouselScrollFrom) * eased);
|
||||
if (progress >= 1)
|
||||
{
|
||||
_carouselAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RequestAnimationFrame(OnCarouselAnimationFrame);
|
||||
}
|
||||
|
||||
private void OnGameListDoubleTapped(object? sender, TappedEventArgs e)
|
||||
|
|
|
|||
|
|
@ -9,17 +9,21 @@ Cover-art library item states and motion.
|
|||
<Style Selector="ListBox.tileGrid">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="4,0,8,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="-32,0,-32,0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
|
||||
<Setter Property="ItemsPanel">
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.tileGrid.gridLayout">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="4,0,8,0" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
|
||||
|
|
@ -30,6 +34,7 @@ Cover-art library item states and motion.
|
|||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||
<Setter Property="FocusAdorner" Value="{x:Null}" />
|
||||
<Setter Property="Width" Value="{DynamicResource LibraryTileWidth}" />
|
||||
<Setter Property="Height" Value="{DynamicResource LibraryRailItemHeight}" />
|
||||
<Setter Property="Padding" Value="4" />
|
||||
|
|
|
|||
|
|
@ -230,7 +230,9 @@ public static partial class ImageRequestBuilders
|
|||
}
|
||||
else
|
||||
{
|
||||
pitch = width;
|
||||
// A linear color buffer uses the same 256-byte row alignment as a linear texture of the same memory;
|
||||
// an unaligned pitch made the target and a later texture view of it two different cached images.
|
||||
pitch = TileGeometry.TexturePitch(transferFormat, width, GuestTileMode.Linear);
|
||||
}
|
||||
|
||||
var mipSpans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
||||
|
|
|
|||
|
|
@ -240,6 +240,21 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
|
|||
Assert.Equal(0u, ImageRequestBuilders.SampleCount(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTarget_LinearTargetPitchMatchesTheLinearTexturePitch()
|
||||
{
|
||||
var target = ImageRequestBuilders.ColorTarget(RegisterWords.Color(Base, 816, 604), 0xF, 0, false);
|
||||
var texture = ImageRequestBuilders.Texture(RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 816, 604), Sampled2D);
|
||||
|
||||
Assert.NotNull(target);
|
||||
var description = target.Value.Request.Description;
|
||||
Assert.Equal(new Extent3D(816, 604, 1), description.Extent);
|
||||
Assert.Equal(832u, description.Pitch);
|
||||
Assert.Equal(832UL * 604 * 4, description.Data.Size);
|
||||
Assert.Equal(texture.Request.Description.Pitch, description.Pitch);
|
||||
Assert.Equal(texture.Request.Description.Data.Size, description.Data.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Texture_LinearTwoDimensionalFromDescriptor()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue