From 77f22973cb0ef0db12d901caac09371b9ba93e05 Mon Sep 17 00:00:00 2001 From: Daniel Freak Date: Thu, 30 Jul 2026 02:47:15 +0300 Subject: [PATCH] [GUI] Update library page layout (#690) * [GUI] Add horizontal game tiles * [GUI] Update launching elements layout * [GUI] Change cards format to squares --- src/SharpEmu.GUI/AddFolderTile.cs | 16 + src/SharpEmu.GUI/GameEntry.cs | 2 +- src/SharpEmu.GUI/Languages/ar.json | 2 +- src/SharpEmu.GUI/Languages/br.json | 2 +- src/SharpEmu.GUI/Languages/de.json | 2 +- src/SharpEmu.GUI/Languages/dk.json | 2 +- src/SharpEmu.GUI/Languages/en.json | 2 +- src/SharpEmu.GUI/Languages/es.json | 2 +- src/SharpEmu.GUI/Languages/fr.json | 2 +- src/SharpEmu.GUI/Languages/hu.json | 2 +- src/SharpEmu.GUI/Languages/it.json | 2 +- src/SharpEmu.GUI/Languages/ja.json | 2 +- src/SharpEmu.GUI/Languages/ko.json | 2 +- src/SharpEmu.GUI/Languages/nl.json | 2 +- src/SharpEmu.GUI/Languages/pt.json | 2 +- src/SharpEmu.GUI/Languages/ru.json | 2 +- src/SharpEmu.GUI/Languages/tr.json | 2 +- src/SharpEmu.GUI/LibraryTile.cs | 9 + src/SharpEmu.GUI/LibraryTileCollection.cs | 103 +++++ src/SharpEmu.GUI/MainWindow.axaml | 261 +++++++------ src/SharpEmu.GUI/MainWindow.axaml.cs | 362 ++++++------------ src/SharpEmu.GUI/Themes/Styles/Library.axaml | 121 +++++- .../GUI/LibraryTileCollectionTests.cs | 52 +++ 23 files changed, 542 insertions(+), 414 deletions(-) create mode 100644 src/SharpEmu.GUI/AddFolderTile.cs create mode 100644 src/SharpEmu.GUI/LibraryTile.cs create mode 100644 src/SharpEmu.GUI/LibraryTileCollection.cs create mode 100644 tests/SharpEmu.Libs.Tests/GUI/LibraryTileCollectionTests.cs diff --git a/src/SharpEmu.GUI/AddFolderTile.cs b/src/SharpEmu.GUI/AddFolderTile.cs new file mode 100644 index 00000000..44facad9 --- /dev/null +++ b/src/SharpEmu.GUI/AddFolderTile.cs @@ -0,0 +1,16 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.GUI; + +/// +/// Stateless trailing action that opens the library folder picker. +/// +public sealed class AddFolderTile : LibraryTile +{ + public static AddFolderTile Instance { get; } = new(); + + private AddFolderTile() + { + } +} diff --git a/src/SharpEmu.GUI/GameEntry.cs b/src/SharpEmu.GUI/GameEntry.cs index e0d2cc37..0d84be29 100644 --- a/src/SharpEmu.GUI/GameEntry.cs +++ b/src/SharpEmu.GUI/GameEntry.cs @@ -17,7 +17,7 @@ internal enum GameEntryChanges Background = 4, } -public sealed class GameEntry : INotifyPropertyChanged +public sealed class GameEntry : LibraryTile, INotifyPropertyChanged { // Placeholder gradients for games without cover art, picked // deterministically from the game name so a game keeps its color. diff --git a/src/SharpEmu.GUI/Languages/ar.json b/src/SharpEmu.GUI/Languages/ar.json index 115c3155..519e7ef6 100644 --- a/src/SharpEmu.GUI/Languages/ar.json +++ b/src/SharpEmu.GUI/Languages/ar.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} لعبة", "Library.SearchWatermark": "ابحث في المكتبة...", - "Library.AddFolder": "+ إضافة مجلد", + "Library.AddFolder": "إضافة مجلد", "Library.OpenFile": "فتح ملف...", "Library.Context.Launch": "تشغيل", diff --git a/src/SharpEmu.GUI/Languages/br.json b/src/SharpEmu.GUI/Languages/br.json index dcc0e219..9938ab67 100644 --- a/src/SharpEmu.GUI/Languages/br.json +++ b/src/SharpEmu.GUI/Languages/br.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} jogos", "Library.SearchWatermark": "Pesquisar na biblioteca…", - "Library.AddFolder": "+ Adicionar pasta", + "Library.AddFolder": "Adicionar pasta", "Library.OpenFile": "Abrir arquivo…", "Library.Context.Launch": "Jogar", diff --git a/src/SharpEmu.GUI/Languages/de.json b/src/SharpEmu.GUI/Languages/de.json index cc936913..481ab6a1 100644 --- a/src/SharpEmu.GUI/Languages/de.json +++ b/src/SharpEmu.GUI/Languages/de.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} Spiele", "Library.SearchWatermark": "Bibliothek durchsuchen…", - "Library.AddFolder": "+ Spielordner hinzufügen", + "Library.AddFolder": "Spielordner hinzufügen", "Library.OpenFile": "Datei öffnen…", "Library.Context.Launch": "Starten", diff --git a/src/SharpEmu.GUI/Languages/dk.json b/src/SharpEmu.GUI/Languages/dk.json index 479f8d59..a5ad45d7 100644 --- a/src/SharpEmu.GUI/Languages/dk.json +++ b/src/SharpEmu.GUI/Languages/dk.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} spil", "Library.SearchWatermark": "Søg i biblioteket…", - "Library.AddFolder": "+ Tilføj mappe", + "Library.AddFolder": "Tilføj mappe", "Library.OpenFile": "Åbn fil…", "Library.Context.Launch": "Start", diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json index 8a59c75e..f734c547 100644 --- a/src/SharpEmu.GUI/Languages/en.json +++ b/src/SharpEmu.GUI/Languages/en.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} games", "Library.SearchWatermark": "Search library…", - "Library.AddFolder": "+ Add folder", + "Library.AddFolder": "Add folder", "Library.OpenFile": "Open file…", "Library.Context.Launch": "Launch", diff --git a/src/SharpEmu.GUI/Languages/es.json b/src/SharpEmu.GUI/Languages/es.json index 53d6d473..8838260e 100644 --- a/src/SharpEmu.GUI/Languages/es.json +++ b/src/SharpEmu.GUI/Languages/es.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} juegos", "Library.SearchWatermark": "Buscar en la biblioteca…", - "Library.AddFolder": "+ Añadir carpeta", + "Library.AddFolder": "Añadir carpeta", "Library.OpenFile": "Abrir archivo…", "Library.Context.Launch": "Iniciar", diff --git a/src/SharpEmu.GUI/Languages/fr.json b/src/SharpEmu.GUI/Languages/fr.json index 5686ed9c..8d753f24 100644 --- a/src/SharpEmu.GUI/Languages/fr.json +++ b/src/SharpEmu.GUI/Languages/fr.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} jeux", "Library.SearchWatermark": "Rechercher dans la bibliothèque…", - "Library.AddFolder": "+ Ajouter un dossier", + "Library.AddFolder": "Ajouter un dossier", "Library.OpenFile": "Ouvrir un fichier…", "Library.Context.Launch": "Lancer", diff --git a/src/SharpEmu.GUI/Languages/hu.json b/src/SharpEmu.GUI/Languages/hu.json index b2594ee0..59e807af 100644 --- a/src/SharpEmu.GUI/Languages/hu.json +++ b/src/SharpEmu.GUI/Languages/hu.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} játékok", "Library.SearchWatermark": "Keresés a könyvtárban", - "Library.AddFolder": "+ Mappa hozzáadása", + "Library.AddFolder": "Mappa hozzáadása", "Library.OpenFile": "Fájl megnyitása…", "Library.Context.Launch": "Inditás", diff --git a/src/SharpEmu.GUI/Languages/it.json b/src/SharpEmu.GUI/Languages/it.json index 216dda7f..54b8852d 100644 --- a/src/SharpEmu.GUI/Languages/it.json +++ b/src/SharpEmu.GUI/Languages/it.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} giochi", "Library.SearchWatermark": "Cerca nella libreria…", - "Library.AddFolder": "+ Aggiungi cartella", + "Library.AddFolder": "Aggiungi cartella", "Library.OpenFile": "Apri file…", "Library.Context.Launch": "Avvia", diff --git a/src/SharpEmu.GUI/Languages/ja.json b/src/SharpEmu.GUI/Languages/ja.json index 80a5afab..c8e7c377 100644 --- a/src/SharpEmu.GUI/Languages/ja.json +++ b/src/SharpEmu.GUI/Languages/ja.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "ゲーム {0}本", "Library.SearchWatermark": "ライブラリを検索…", - "Library.AddFolder": "+ フォルダーを追加", + "Library.AddFolder": "フォルダーを追加", "Library.OpenFile": "ファイルを開く…", "Library.Context.Launch": "起動", diff --git a/src/SharpEmu.GUI/Languages/ko.json b/src/SharpEmu.GUI/Languages/ko.json index b66deee8..05f086bc 100644 --- a/src/SharpEmu.GUI/Languages/ko.json +++ b/src/SharpEmu.GUI/Languages/ko.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "게임 {0}개", "Library.SearchWatermark": "라이브러리 검색…", - "Library.AddFolder": "+ 폴더 추가", + "Library.AddFolder": "폴더 추가", "Library.OpenFile": "파일 열기…", "Library.Context.Launch": "실행", diff --git a/src/SharpEmu.GUI/Languages/nl.json b/src/SharpEmu.GUI/Languages/nl.json index 424ec6c3..32653d3b 100644 --- a/src/SharpEmu.GUI/Languages/nl.json +++ b/src/SharpEmu.GUI/Languages/nl.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} games", "Library.SearchWatermark": "Zoeken in bibliotheek…", - "Library.AddFolder": "+ Map toevoegen", + "Library.AddFolder": "Map toevoegen", "Library.OpenFile": "Bestand openen…", "Library.Context.Launch": "Starten", diff --git a/src/SharpEmu.GUI/Languages/pt.json b/src/SharpEmu.GUI/Languages/pt.json index f8e93d9c..838bd72f 100644 --- a/src/SharpEmu.GUI/Languages/pt.json +++ b/src/SharpEmu.GUI/Languages/pt.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} jogos", "Library.SearchWatermark": "Pesquisar biblioteca…", - "Library.AddFolder": "+ Adicionar pasta", + "Library.AddFolder": "Adicionar pasta", "Library.OpenFile": "Abrir ficheiro…", "Library.Context.Launch": "Iniciar", diff --git a/src/SharpEmu.GUI/Languages/ru.json b/src/SharpEmu.GUI/Languages/ru.json index 423cb206..79d1858c 100644 --- a/src/SharpEmu.GUI/Languages/ru.json +++ b/src/SharpEmu.GUI/Languages/ru.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "Игр: {0}", "Library.SearchWatermark": "Поиск…", - "Library.AddFolder": "+ Добавить папку", + "Library.AddFolder": "Добавить папку", "Library.OpenFile": "Открыть файл…", "Library.Context.Launch": "Запустить", diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json index 3a26330a..c7c96ef9 100644 --- a/src/SharpEmu.GUI/Languages/tr.json +++ b/src/SharpEmu.GUI/Languages/tr.json @@ -7,7 +7,7 @@ "Page.GameCount.Other": "{0} oyun", "Library.SearchWatermark": "Kütüphanede ara…", - "Library.AddFolder": "+ Klasör ekle", + "Library.AddFolder": "Klasör ekle", "Library.OpenFile": "Dosya aç…", "Library.Context.Launch": "Başlat", diff --git a/src/SharpEmu.GUI/LibraryTile.cs b/src/SharpEmu.GUI/LibraryTile.cs new file mode 100644 index 00000000..9106bcbf --- /dev/null +++ b/src/SharpEmu.GUI/LibraryTile.cs @@ -0,0 +1,9 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.GUI; + +/// +/// Base type for game cards and actions shown in the library rail. +/// +public abstract class LibraryTile; diff --git a/src/SharpEmu.GUI/LibraryTileCollection.cs b/src/SharpEmu.GUI/LibraryTileCollection.cs new file mode 100644 index 00000000..6571bd86 --- /dev/null +++ b/src/SharpEmu.GUI/LibraryTileCollection.cs @@ -0,0 +1,103 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace SharpEmu.GUI; + +/// +/// Read-only view of the visible games with one persistent action tile at +/// the end. Game collection changes keep their original indices, so Avalonia +/// can update and virtualize the rail without rebuilding every item. +/// +public sealed class LibraryTileCollection : IReadOnlyList, IList, INotifyCollectionChanged +{ + private readonly ObservableCollection _games; + + public LibraryTileCollection(ObservableCollection games) + { + _games = games; + _games.CollectionChanged += OnGamesCollectionChanged; + } + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public int Count => _games.Count + 1; + + bool IList.IsFixedSize => false; + + bool IList.IsReadOnly => true; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + public LibraryTile this[int index] + { + get + { + if ((uint)index < (uint)_games.Count) + { + return _games[index]; + } + + if (index == _games.Count) + { + return AddFolderTile.Instance; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + object? IList.this[int index] + { + get => this[index]; + set => throw new NotSupportedException(); + } + + public IEnumerator GetEnumerator() + { + foreach (var game in _games) + { + yield return game; + } + + yield return AddFolderTile.Instance; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + int IList.Add(object? value) => throw new NotSupportedException(); + + void IList.Clear() => throw new NotSupportedException(); + + bool IList.Contains(object? value) => ((IList)this).IndexOf(value) >= 0; + + int IList.IndexOf(object? value) => value switch + { + GameEntry game => _games.IndexOf(game), + AddFolderTile => _games.Count, + _ => -1, + }; + + void IList.Insert(int index, object? value) => throw new NotSupportedException(); + + void IList.Remove(object? value) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int index) + { + ArgumentNullException.ThrowIfNull(array); + foreach (var tile in this) + { + array.SetValue(tile, index++); + } + } + + private void OnGamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); +} diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index 74ead40a..18d3ebaa 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -6,7 +6,6 @@ SPDX-License-Identifier: GPL-2.0-or-later - + - + + + Grid.Row="0" + SelectionMode="Single" Padding="4,0,28,0"> - + - + - - - - - - - - - - + + + + + + + + - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + +