From 48a694e509ec3ecdbdb776ecc4b9f509bf159352 Mon Sep 17 00:00:00 2001 From: Brando Date: Sat, 11 Jul 2026 06:17:10 -0400 Subject: [PATCH] GUI: redesign library as a cover-art grid with game management (#48) * GUI: redesign library as a cover-art grid with game management Replace the sidebar game list with a full-width grid of cover tiles. Cover art is loaded automatically from each game's sce_sys/icon0.png (pic0.png fallback) and decoded off the UI thread; games without art get a deterministic gradient placeholder with the title's initials. - New layout: search/scan toolbar, tile grid with hover and selection states, bottom launch bar with cover thumbnail, collapsible launch options and console panels (console auto-opens on launch) - Right-click context menu on tiles: launch, open game folder, copy path/title ID, remove from library - Removed games persist in an ExcludedGames settings list; re-adding a folder restores any removed games beneath it - Search now also matches title IDs - Fix: placeholder brushes were constructed on the scan thread, which throws in Avalonia and was silently swallowed, yielding empty scans * GUI: show full install folder size instead of eboot.bin size The library previously displayed the size of eboot.bin alone, which wildly understates a game's real footprint. The install folder is now totaled recursively in the existing background pass (after cover art, which is cheaper and more visible), and each tile updates live once its size is ready. * GUI: selection backdrop, smaller tiles, controller navigation Address review feedback on the library redesign: - Selecting a game fades its key art (sce_sys/pic0.png, pic1.png fallback) in as the window backdrop, dimmed by a gradient scrim; decoded off the UI thread and cached per entry - Cover tiles reduced from 156px to 128px - The library can be driven with a DualSense: d-pad/left stick moves the selection (hold-to-repeat, row-aware), Cross launches, Circle stops; input is ignored while the launcher window is unfocused. Reuses the pad HID reader by compile-linking its dependency-free sources instead of referencing all of SharpEmu.Libs --- src/SharpEmu.GUI/App.axaml | 92 +++++- src/SharpEmu.GUI/GameEntry.cs | 138 +++++++- src/SharpEmu.GUI/GuiSettings.cs | 3 + src/SharpEmu.GUI/MainWindow.axaml | 299 +++++++++++------ src/SharpEmu.GUI/MainWindow.axaml.cs | 468 ++++++++++++++++++++++++++- src/SharpEmu.GUI/SharpEmu.GUI.csproj | 8 + 6 files changed, 895 insertions(+), 113 deletions(-) diff --git a/src/SharpEmu.GUI/App.axaml b/src/SharpEmu.GUI/App.axaml index 1638b9d..8b73f44 100644 --- a/src/SharpEmu.GUI/App.axaml +++ b/src/SharpEmu.GUI/App.axaml @@ -11,7 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later #7C5CFC - + + + + + + @@ -24,6 +29,8 @@ SPDX-License-Identifier: GPL-2.0-or-later + + @@ -38,7 +45,7 @@ SPDX-License-Identifier: GPL-2.0-or-later - + @@ -61,6 +68,10 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + + + + + + + + - + + + + + + + diff --git a/src/SharpEmu.GUI/GameEntry.cs b/src/SharpEmu.GUI/GameEntry.cs index ea27d3b..c13732a 100644 --- a/src/SharpEmu.GUI/GameEntry.cs +++ b/src/SharpEmu.GUI/GameEntry.cs @@ -1,13 +1,147 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.ComponentModel; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; + namespace SharpEmu.GUI; -public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes) +public sealed class GameEntry : INotifyPropertyChanged { + // Placeholder gradients for games without cover art, picked + // deterministically from the game name so a game keeps its color. + private static readonly (Color Start, Color End)[] PlaceholderPalette = + { + (Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")), + (Color.Parse("#1F6E8C"), Color.Parse("#173B45")), + (Color.Parse("#7A4069"), Color.Parse("#3B1C32")), + (Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")), + (Color.Parse("#8C5425"), Color.Parse("#4A2B12")), + (Color.Parse("#4F6D9E"), Color.Parse("#263349")), + (Color.Parse("#8A4B4B"), Color.Parse("#3F2222")), + (Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")), + }; + + private Bitmap? _cover; + private IBrush? _placeholderBrush; + private long _sizeBytes; + + public GameEntry( + string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath) + { + Name = name; + TitleId = titleId; + Path = path; + _sizeBytes = sizeBytes; + CoverPath = coverPath; + BackgroundPath = backgroundPath; + Initials = ComputeInitials(name); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + public string Name { get; } + + public string? TitleId { get; } + + public string Path { get; } + + /// + /// Total size of the game. Initially the eboot's own size from the scan; + /// replaced with the full install folder size once computed in the + /// background. + /// + public long SizeBytes + { + get => _sizeBytes; + set + { + if (_sizeBytes == value) + { + return; + } + + _sizeBytes = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail))); + } + } + + /// Path to the cover art image shipped with the game, if found. + public string? CoverPath { get; } + + /// Path to the key art (pic0/pic1) shipped with the game, if found. + public string? BackgroundPath { get; } + + /// + /// Decoded key art used as the window backdrop while this game is + /// selected. Loaded on demand and cached; not exposed via binding. + /// + public Bitmap? Background { get; set; } + + public string Initials { get; } + + // Built lazily: brushes are AvaloniaObjects that must be created on the + // UI thread, while GameEntry itself is constructed on the scan thread. + public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name); + + /// Decoded cover art; loaded asynchronously after the library scan. + public Bitmap? Cover + { + get => _cover; + set + { + if (ReferenceEquals(_cover, value)) + { + return; + } + + _cover = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover))); + } + } + + public bool HasCover => _cover is not null; + public string Detail => TitleId is not null ? $"{TitleId} • {FormatSize(SizeBytes)}" - : $"{FormatSize(SizeBytes)} • {Path}"; + : FormatSize(SizeBytes); + + private static string ComputeInitials(string name) + { + var initials = name + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(word => char.IsLetterOrDigit(word[0])) + .Select(word => char.ToUpperInvariant(word[0])) + .Take(2) + .ToArray(); + + return initials.Length > 0 ? new string(initials) : "?"; + } + + private static IBrush BuildPlaceholderBrush(string name) + { + var hash = 0; + foreach (var ch in name) + { + hash = unchecked(hash * 31 + ch); + } + + var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)]; + return new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), + GradientStops = + { + new GradientStop(start, 0), + new GradientStop(end, 1), + }, + }; + } private static string FormatSize(long bytes) { diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs index bc31717..5ffaf3b 100644 --- a/src/SharpEmu.GUI/GuiSettings.cs +++ b/src/SharpEmu.GUI/GuiSettings.cs @@ -14,6 +14,9 @@ public sealed class GuiSettings public List GameFolders { get; set; } = new(); + /// Eboot paths hidden from the library via "Remove from library". + public List ExcludedGames { get; set; } = new(); + public string LogLevel { get; set; } = "Info"; public int ImportTraceLimit { get; set; } diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index 2ebd422..3d8ad76 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SharpEmu.GUI.MainWindow" Title="SharpEmu" - Width="1280" Height="800" - MinWidth="980" MinHeight="620" + Width="1280" Height="820" + MinWidth="980" MinHeight="640" WindowStartupLocation="CenterScreen" Background="{StaticResource BgBrush}" ExtendClientAreaToDecorationsHint="True" @@ -18,6 +18,28 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + + + + + + + + + + + + + + + + + + @@ -30,119 +52,204 @@ SPDX-License-Identifier: GPL-2.0-or-later - + - - - - - - - - + + + + + + + +