diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs
index 2634830c..f365613b 100644
--- a/src/SharpEmu.GUI/GuiSettings.cs
+++ b/src/SharpEmu.GUI/GuiSettings.cs
@@ -40,6 +40,8 @@ public sealed class GuiSettings
/// Loop the selected game's sce_sys/snd0.at9 preview music.
public bool PlayTitleMusic { get; set; } = true;
+ public string LibraryLayout { get; set; } = "Carousel";
+
public string? EmulatorPath { get; set; }
/// UI language, matching a file code under Languages/ (e.g. "en", "tr").
@@ -132,6 +134,7 @@ public sealed class GuiSettings
{
settings.RenderResolutionScale = 1.0;
}
+ settings.LibraryLayout = NormalizeChoice(settings.LibraryLayout, "Carousel", "Grid");
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
settings.Resolution = NormalizeResolution(settings.Resolution);
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json
index 30bc6734..f0b8af9f 100644
--- a/src/SharpEmu.GUI/Languages/en.json
+++ b/src/SharpEmu.GUI/Languages/en.json
@@ -9,6 +9,8 @@
"Library.SearchWatermark": "Search library…",
"Library.AddFolder": "Add folder",
"Library.OpenFile": "Open file…",
+ "Library.View.Grid": "Stacked view",
+ "Library.View.Carousel": "Row view",
"Library.Context.Launch": "Launch",
"Library.Context.OpenFolder": "Open game folder",
diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json
index ad074051..ab7fdc09 100644
--- a/src/SharpEmu.GUI/Languages/tr.json
+++ b/src/SharpEmu.GUI/Languages/tr.json
@@ -9,6 +9,8 @@
"Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": "Klasör ekle",
"Library.OpenFile": "Dosya aç…",
+ "Library.View.Grid": "Alt alta görünüm",
+ "Library.View.Carousel": "Tek sıra görünüm",
"Library.Context.Launch": "Başlat",
"Library.Context.OpenFolder": "Oyun klasörünü aç",
diff --git a/src/SharpEmu.GUI/MainWindow.GameOptions.cs b/src/SharpEmu.GUI/MainWindow.GameOptions.cs
index ac0f6e95..a2bb69d2 100644
--- a/src/SharpEmu.GUI/MainWindow.GameOptions.cs
+++ b/src/SharpEmu.GUI/MainWindow.GameOptions.cs
@@ -476,18 +476,6 @@ public partial class MainWindow
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
];
- private static void SetGameOptionsOpenClass(Control control, bool active)
- {
- if (active)
- {
- if (!control.Classes.Contains("gameOptionsOpen"))
- {
- control.Classes.Add("gameOptionsOpen");
- }
- }
- else
- {
- control.Classes.Remove("gameOptionsOpen");
- }
- }
+ private static void SetGameOptionsOpenClass(Control control, bool active) =>
+ SetClass(control, "gameOptionsOpen", active);
}
diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml
index 9ba5ee1e..c56abd09 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml
+++ b/src/SharpEmu.GUI/MainWindow.axaml
@@ -136,6 +136,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+ SelectionMode="Single">
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+ Margin="4,8,0,0">
- ConsoleToggle.IsChecked = false;
LibraryTabButton.Click += (_, _) => SetActivePage(0);
OptionsTabButton.Click += (_, _) => SetActivePage(1);
+ LibraryLayoutButton.Click += (_, _) => ToggleLibraryLayout();
+ LibraryPage.SizeChanged += (_, _) => UpdateLibraryGridHeight();
+ LibrarySelectedDetails.SizeChanged += (_, _) => UpdateLibraryGridHeight();
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
WireOptionsNavigation();
WireGameOptions();
@@ -379,18 +385,73 @@ public partial class MainWindow : Window
}
}
- private static void SetActiveClass(Button button, bool active)
+ private void SetLibraryLayout(bool grid)
+ {
+ _isLibraryGridLayout = grid;
+ SetClass(GameList, "gridLayout", grid);
+ SetClass(LibrarySelectedDetails, "gridLayout", grid);
+ LibraryPage.RowDefinitions[0].Height = grid
+ ? GridLength.Auto
+ : new GridLength(188);
+ LibraryPage.Margin = grid
+ ? new Thickness(0, 6, 0, 0)
+ : new Thickness(0, 46, 0, 0);
+ UpdateLibraryGridHeight();
+ UpdateLibraryLayoutButton();
+
+ if (GameList.SelectedItem is { } selected)
+ {
+ Dispatcher.UIThread.Post(
+ () => GameList.ScrollIntoView(selected),
+ DispatcherPriority.Loaded);
+ }
+ }
+
+ private void UpdateLibraryGridHeight()
+ {
+ var pageHeight = LibraryPage.Bounds.Height;
+ if (!_isLibraryGridLayout || pageHeight <= 0)
+ {
+ GameList.MaxHeight = double.PositiveInfinity;
+ return;
+ }
+
+ GameList.MaxHeight = Math.Max(
+ 0,
+ pageHeight - LibrarySelectedDetails.DesiredSize.Height);
+ }
+
+ private void ToggleLibraryLayout()
+ {
+ SetLibraryLayout(!_isLibraryGridLayout);
+ _settings.LibraryLayout = _isLibraryGridLayout ? "Grid" : "Carousel";
+ _settings.Save();
+ }
+
+ private void UpdateLibraryLayoutButton()
+ {
+ LibraryLayoutGlyph.Text = _isLibraryGridLayout ? "view_carousel" : "grid_view";
+ var label = Localization.Instance.Get(
+ _isLibraryGridLayout ? "Library.View.Carousel" : "Library.View.Grid");
+ ToolTip.SetTip(LibraryLayoutButton, label);
+ AutomationProperties.SetName(LibraryLayoutButton, label);
+ }
+
+ private static void SetActiveClass(Button button, bool active) =>
+ SetClass(button, "active", active);
+
+ private static void SetClass(Control control, string className, bool active)
{
if (active)
{
- if (!button.Classes.Contains("active"))
+ if (!control.Classes.Contains(className))
{
- button.Classes.Add("active");
+ control.Classes.Add(className);
}
}
else
{
- button.Classes.Remove("active");
+ control.Classes.Remove(className);
}
}
@@ -676,6 +737,23 @@ public partial class MainWindow : Window
MoveSelection(1);
}
+ if (_isLibraryGridLayout)
+ {
+ var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
+ var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
+ var rowStep = LibraryRowStep();
+
+ if (ShouldNavigate(up, ref _navUpNextAt, now))
+ {
+ MoveSelection(-rowStep);
+ }
+
+ if (ShouldNavigate(down, ref _navDownNextAt, now))
+ {
+ MoveSelection(rowStep);
+ }
+ }
+
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & HostGamepadButtons.Cross) != 0)
{
@@ -712,6 +790,29 @@ public partial class MainWindow : Window
return false;
}
+ private int LibraryRowStep()
+ {
+ if (GameList.ContainerFromIndex(0) is not { } first)
+ {
+ return 1;
+ }
+
+ var top = first.Bounds.Top;
+ var columns = 1;
+ for (var index = 1; index < _libraryTiles.Count; index++)
+ {
+ if (GameList.ContainerFromIndex(index) is not { } container ||
+ Math.Abs(container.Bounds.Top - top) > 0.5)
+ {
+ break;
+ }
+
+ columns++;
+ }
+
+ return columns;
+ }
+
private void MoveSelection(int delta)
{
var index = GameList.SelectedIndex < 0
@@ -783,6 +884,7 @@ public partial class MainWindow : Window
RefreshHostRefreshRates(_settings.RefreshRate);
RefreshUpdateText();
UpdateEmptyStateTexts();
+ UpdateLibraryLayoutButton();
UpdateRunButtons();
}
@@ -1067,6 +1169,7 @@ public partial class MainWindow : Window
LogToFileToggle.IsChecked = _settings.LogToFile;
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
+ SetLibraryLayout(string.Equals(_settings.LibraryLayout, "Grid", StringComparison.OrdinalIgnoreCase));
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
diff --git a/src/SharpEmu.GUI/Themes/Styles/Buttons.axaml b/src/SharpEmu.GUI/Themes/Styles/Buttons.axaml
index 914a41f0..9f632575 100644
--- a/src/SharpEmu.GUI/Themes/Styles/Buttons.axaml
+++ b/src/SharpEmu.GUI/Themes/Styles/Buttons.axaml
@@ -43,6 +43,23 @@ Shared launcher button variants and page switcher styles.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+