UI/HomeScreen.razor
@using System.Threading.Tasks
@namespace sGBA
@inherits PanelComponent
<root class="@RootClass">
<div class="home-screen">
<HomeBackdrop Current=@_currentBackdrop Previous=@_previousBackdrop PreviousOpacity=@PreviousBackdropOpacity />
@if (ToastVisible)
{
<HomeToast Title=@_toast.Title Message=@_toast.Message Icon=@_toast.Icon ColorClass=@_toast.ColorClass InlineStyle=@ToastStyle />
}
@if (_allSoftwareOpen)
{
<div class="all-software-screen">
<div class="all-software-rule top" />
<div class="all-software-rule bottom" />
<div class="all-software-scroll" @ref=AllSoftwareScroll>
<div class="all-software-grid" style="@AllSoftwareGridStyle">
@foreach (var gridItem in AllSoftwareGridItems)
{
Texture gridCover = null;
if (gridItem.Game != null)
_boxArt.TryGetValue(gridItem.Game.Path, out gridCover);
<AllSoftwareCard @key="gridItem.Key"
[email protected]
Cover=@gridCover
Selected=@IsAllSoftwareSelected(gridItem.Index)
PositionStyle=@AllSoftwareGridItemStyle(gridItem.Row, gridItem.Column)
CardLeft=@(gridItem.Column * (AllSoftwareCardWidth + AllSoftwareColumnGap))
CardWidth=@AllSoftwareCardWidth
GridWidth=@AllSoftwareGridWidth
ShowTitleOnTop=@ShouldShowAllSoftwareTitleOnTop(gridItem.Index)
OnActivate=@(() => ActivateAllSoftwareItemWithMouse(gridItem.Index)) />
}
<div class="all-add-row" style="@AllSoftwareAddRowStyle">
<div class="@AllSoftwareAddButtonClass" onmouseenter=@UseMouseInAllSoftware onclick=@ActivateAllSoftwareAdd>
<SelectionRing Active=@IsAllSoftwareAddSelected StrokeWidth=@(8f) CornerRadius=@(10f) Gap=@(0f) class="all-add-button-ring" />
<div class="all-add-button-fill" />
<div class="all-add-button-label">#addgames.card.label</div>
</div>
</div>
</div>
</div>
@if (AllSoftwareShowsScrollbar)
{
<div class="all-scrollbar" @ref=AllSoftwareScrollbar onmousedown=@OnAllSoftwareScrollbarMouseDown>
<div class="all-scrollbar-thumb" @ref=AllSoftwareScrollbarThumb style="@AllSoftwareScrollbarThumbStyle" />
</div>
}
</div>
}
else
{
<div class="profile-avatar" style="background-image: url( avatar:@LocalSteamId )" />
@if (!IsHomeViewMoreSelected)
{
<HomeLogo Logo=@SelectedLogo Title=@SelectedTitle />
}
<HomeCarousel Items=@CarouselItems SelectedIndex=@_selectedIndex NavFocused=@_navFocused ShowSelection=@ShowHomeSelection RenderVersion=@_homeCarouselRenderVersion ArtworkVersion=@_artworkVersion MountedRange=@CarouselMountedRange Suppress=@_suppressHomeCarouselTransitions CoverFor=@((GameEntry g) => _boxArt.GetValueOrDefault(g.Path)) OnActivate=@((int index) => ActivateCarouselItem(index)) />
<HomeNavPill Items=@NavItems SelectedIndex=@_navSelection Focused=@_navFocused ShowSelection=@(ShowHomeSelection && _input.UseGamepad && _navFocused) OnHover=@((int i) => SelectNavItem(i, useGamepad: false)) OnActivate=@((int i) => ActivateNavItem(i, useGamepad: false)) />
}
</div>
</root>
@code
{
public static HomeScreen Current { get; private set; }
private List<GameEntry> _games = new();
private List<GameEntry> _allGames = new();
private List<GameEntry> _homeItems = new();
private HashSet<string> _knownPaths = new();
private float _pollTimer;
private const float PollInterval = 3f;
private Dictionary<string, Texture> _boxArt = new();
private Dictionary<string, Texture> _logos = new();
private Dictionary<string, Texture> _snaps = new();
private Dictionary<string, Texture> _titles = new();
private int _artworkVersion;
private int _artworkRequestId;
private int _selectedIndex;
private int _allSoftwareSelection;
private bool _homeOrderDirty;
private bool _suppressHomeCarouselTransitions;
private int _homeCarouselTransitionSuppressionFrames;
private int _homeCarouselRenderVersion;
private float _homeCarouselSettleRemaining;
private GameEntry _queuedHomeLaunchGame;
private string _pendingFrozenHomeLaunchPath;
private int _pendingFrozenHomeLaunchFrames;
private string _pendingHomeSelectionPath;
private int _navSelection = 2;
private bool _navFocused;
private bool _isVisible = true;
private bool _allSoftwareOpen;
private bool _addGamesModalOpen;
private readonly HomeToastState _toast = new();
private const int HomeGameLimit = 12;
private const int CarouselMountedRange = 9;
private const float BackdropRetainDuration = 0.32f;
private Texture _currentBackdrop;
private Texture _previousBackdrop;
private float _previousBackdropAge;
private bool _previousBackdropFading;
private int _backdropVersion;
private readonly List<PendingTextureDispose> _pendingTextureDisposals = new();
private const float HomeCarouselSettleDuration = 0.2f;
private sealed record ArtworkRequest(GameEntry Game, ThumbType Kind, Dictionary<string, Texture> Target);
private sealed record ArtworkLoad(ArtworkRequest Request, Texture Texture);
private readonly record struct PendingTextureDispose(string Key, Texture Texture, float Age);
private readonly NavItem[] NavItems =
[
new("group", "orange", "#nav.netplay"),
new("newspaper", "green", "#nav.news"),
new("cloud", "blue", "#nav.cloud"),
new("settings", "gray", "#nav.settings"),
new("power_settings_new", "gray", "#nav.quit")
];
private readonly FocusInput _input = new();
public bool IsVisible => _isVisible;
private string RootClass => (IsVisible ? "visible" : "") + (_suppressHomeCarouselTransitions ? " suppress-home-carousel-transitions" : "");
public bool AllSoftwareOpen => _allSoftwareOpen;
public bool NavigationFocused => _navFocused;
public bool HasSelectedGame => SelectedGame != null;
public bool CanOpenDetails => !_navFocused && SelectedGame != null;
public bool HasHomeAction => IsHomeViewMoreSelected;
public string PrimaryActionLabel => _allSoftwareOpen || IsHomeViewMoreSelected ? "#prompt.ok" : "#prompt.start";
private long LocalSteamId => Connection.Local is null ? 0L : Connection.Local.SteamId;
private int HomeGameCount => Math.Min(_homeItems.Count, HomeGameLimit);
private int HomeEntryCount => HomeGameCount + 1;
private int HomeViewMoreIndex => HomeGameCount;
private bool IsHomeViewMoreSelected => _selectedIndex == HomeViewMoreIndex;
private bool AddGamesModalOpen => _addGamesModalOpen || AddGamesModal.Current?.IsVisible == true;
private bool SettingsModalOpen => SettingsModal.Current?.IsVisible == true;
private bool ShowHomeSelection => !AddGamesModalOpen && !SettingsModalOpen;
private bool ToastBlocked => AddGamesModalOpen || SettingsModalOpen || DetailsModal.Current?.IsVisible == true;
private bool ToastVisible => _toast.IsVisible(ToastBlocked);
private string ToastStyle => _toast.GetStyle(ToastVisible);
private GameEntry SelectedGame => _allSoftwareOpen
? _allSoftwareSelection >= 0 && _allSoftwareSelection < _allGames.Count ? _allGames[_allSoftwareSelection] : null
: !IsHomeViewMoreSelected && _selectedIndex >= 0 && _selectedIndex < _homeItems.Count ? _homeItems[_selectedIndex] : null;
private string SelectedTitle => SelectedGame?.DisplayTitle ?? "sGBA";
private Texture SelectedLogo => SelectedGame != null && _logos.TryGetValue(SelectedGame.Path, out var logo) ? logo : null;
private Texture SelectedBackdrop => SelectedGame != null && _snaps.TryGetValue(SelectedGame.Path, out var snap) ? snap : SelectedGame != null && _boxArt.TryGetValue(SelectedGame.Path, out var cover) ? cover : null;
private float PreviousBackdropOpacity => _previousBackdrop == null ? 0f : _previousBackdropFading ? 0f : 1f;
private IEnumerable<HomeCarouselEntry> CarouselItems
{
get
{
if (HomeEntryCount == 0)
yield break;
int selected = _selectedIndex.Clamp(0, HomeEntryCount - 1);
int first = Math.Max(0, selected - CarouselMountedRange);
int last = Math.Min(HomeEntryCount - 1, selected + CarouselMountedRange);
for (int i = first; i <= last; i++)
{
bool isViewMore = i == HomeViewMoreIndex;
yield return new HomeCarouselEntry(isViewMore ? null : _homeItems[i], i, i - selected, isViewMore, isViewMore ? "view-more" : _homeItems[i].Path);
}
}
}
protected override void OnTreeFirstBuilt()
{
Current = this;
EnsureInputHintsPanel();
_games = GameEntry.Discover();
_knownPaths = GameEntry.GetInstalledPaths();
RebuildGameLists(preserveSelection: false);
SyncBackdropTexture();
QueueArtworkRefresh();
}
private void QueueArtworkRefresh()
{
var requestId = ++_artworkRequestId;
_ = LoadArtworkAsync(requestId);
}
private async Task LoadArtworkAsync(int requestId)
{
var visibleTask = LoadVisibleCarouselThumbnailsAsync();
var selectedTask = LoadSelectedFeatureArtAsync();
await visibleTask;
await selectedTask;
if (requestId != _artworkRequestId)
return;
var artworkVersion = _artworkVersion;
PruneArtworkTextures();
if (_artworkVersion != artworkVersion)
StateHasChanged();
}
private Task<bool> LoadSelectedFeatureArtAsync()
{
var game = SelectedGame;
if (game == null)
return Task.FromResult(false);
return LoadArtworkBatchAsync([
new ArtworkRequest(game, ThumbType.Snap, _snaps),
new ArtworkRequest(game, ThumbType.Logo, _logos),
new ArtworkRequest(game, ThumbType.Title, _titles)
]);
}
private Task<bool> LoadVisibleCarouselThumbnailsAsync()
{
var homeRequests = CarouselItems
.Where(item => item.Game != null)
.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt));
var allSoftwareRequests = _allSoftwareOpen
? AllSoftwareVisibleItems.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt))
: [];
var requests = homeRequests
.Concat(allSoftwareRequests)
.ToList();
return LoadArtworkBatchAsync(requests);
}
private async Task<bool> LoadArtworkBatchAsync(IReadOnlyList<ArtworkRequest> requests)
{
var pending = requests
.Where(request => request.Game != null && !request.Target.ContainsKey(request.Game.Path))
.ToList();
if (pending.Count == 0)
return false;
foreach (var request in pending)
CancelPendingTextureDispose(GetArtworkKey(request.Kind, request.Game.Path));
var changed = false;
var loadTasks = pending.Select(LoadArtworkAsync).ToList();
while (loadTasks.Count > 0)
{
var completedTask = await Task.WhenAny(loadTasks);
loadTasks.Remove(completedTask);
if (!ApplyArtworkLoad(await completedTask))
continue;
changed = true;
_artworkVersion++;
StateHasChanged();
}
return changed;
}
private static async Task<ArtworkLoad> LoadArtworkAsync(ArtworkRequest request)
{
var texture = await Thumbnails.LoadAsync(request.Game, request.Kind);
return new ArtworkLoad(request, texture);
}
private bool ApplyArtworkLoad(ArtworkLoad load)
{
var game = load.Request.Game;
var kind = load.Request.Kind;
var target = load.Request.Target;
var texture = load.Texture;
if (texture == null || game == null)
return false;
CancelPendingTextureDispose(GetArtworkKey(kind, game.Path));
if (target.ContainsKey(game.Path))
{
return false;
}
if (!ShouldRetainArtwork(game, kind))
{
ScheduleTextureDispose(GetArtworkKey(kind, game.Path), texture);
return false;
}
target[game.Path] = texture;
if (game == SelectedGame)
SyncBackdropTexture();
return true;
}
private bool ShouldRetainArtwork(GameEntry game, ThumbType kind)
{
if (game == null)
return false;
if (game == SelectedGame)
return true;
return kind == ThumbType.BoxArt && RetainedBoxArtPaths().Contains(game.Path);
}
private HashSet<string> RetainedBoxArtPaths()
{
var retained = CarouselItems
.Where(item => IsCarouselOffsetMounted(item.Offset))
.Where(item => item.Game != null)
.Select(item => item.Game.Path)
.ToHashSet();
if (_allSoftwareOpen)
{
foreach (var item in AllSoftwareVisibleItems)
{
if (item.Game != null)
retained.Add(item.Game.Path);
}
}
return retained;
}
private HashSet<string> RetainedFeatureArtworkPaths()
{
var retained = new HashSet<string>();
if (SelectedGame != null)
retained.Add(SelectedGame.Path);
return retained;
}
private void PruneArtworkTextures()
{
PruneArtworkDictionary(_boxArt, ThumbType.BoxArt, RetainedBoxArtPaths());
var retainedFeatureArtwork = RetainedFeatureArtworkPaths();
PruneArtworkDictionary(_logos, ThumbType.Logo, retainedFeatureArtwork);
PruneArtworkDictionary(_snaps, ThumbType.Snap, retainedFeatureArtwork);
PruneArtworkDictionary(_titles, ThumbType.Title, retainedFeatureArtwork);
}
private void PruneArtworkDictionary(Dictionary<string, Texture> textures, ThumbType kind, HashSet<string> retainedPaths)
{
foreach (var (path, texture) in textures.ToList())
{
if (retainedPaths.Contains(path))
continue;
if (texture == _currentBackdrop || texture == _previousBackdrop)
continue;
textures.Remove(path);
ScheduleTextureDispose(GetArtworkKey(kind, path), texture);
_artworkVersion++;
}
}
private static string GetArtworkKey(ThumbType kind, string path)
{
return $"{kind}:{path}";
}
private void ScheduleTextureDispose(string key, Texture texture)
{
if (texture == null)
return;
_pendingTextureDisposals.RemoveAll(item => item.Key == key || item.Texture == texture);
_pendingTextureDisposals.Add(new PendingTextureDispose(key, texture, 0f));
}
private void CancelPendingTextureDispose(string key)
{
_pendingTextureDisposals.RemoveAll(item => item.Key == key);
}
private void UpdatePendingTextureDisposals()
{
if (_pendingTextureDisposals.Count == 0)
return;
for (int i = _pendingTextureDisposals.Count - 1; i >= 0; i--)
{
var pending = _pendingTextureDisposals[i];
var age = pending.Age + Time.Delta;
if (age < 0.5f)
{
_pendingTextureDisposals[i] = pending with { Age = age };
continue;
}
if (!IsArtworkTextureStillReferenced(pending.Texture))
Thumbnails.ReleaseTexture(pending.Texture);
_pendingTextureDisposals.RemoveAt(i);
}
}
private bool IsArtworkTextureStillReferenced(Texture texture)
{
if (texture == null)
return false;
if (texture == _currentBackdrop || texture == _previousBackdrop)
return true;
return _boxArt.ContainsValue(texture) || _logos.ContainsValue(texture) || _snaps.ContainsValue(texture);
}
public void Show()
{
if (_homeOrderDirty)
{
RebuildGameLists(preserveSelection: false);
SelectPendingHomeGame();
_homeOrderDirty = false;
_pendingHomeSelectionPath = null;
_suppressHomeCarouselTransitions = true;
_homeCarouselTransitionSuppressionFrames = 2;
Panel?.SkipTransitions();
SyncBackdropTexture();
QueueArtworkRefresh();
}
_isVisible = true;
_navFocused = false;
_input.Begin(useGamepad: false);
Sound.Play("ui.popup.message.open");
EmulatorComponent.Current?.Unload();
StateHasChanged();
}
public void Hide(bool update = true)
{
_isVisible = false;
Mouse.Visibility = MouseVisibility.Hidden;
if (update)
StateHasChanged();
}
private void ActivateCarouselItem(int index)
{
if (IsHomeViewMoreIndex(index))
{
SelectCarouselItem(index, _input.UseGamepad);
OpenAllSoftware();
return;
}
bool shouldLaunch = !_navFocused && index == _selectedIndex;
SelectCarouselItem(index, _input.UseGamepad);
if (shouldLaunch && SelectedGame != null)
{
if (_homeCarouselSettleRemaining > 0f)
{
_queuedHomeLaunchGame = SelectedGame;
return;
}
LaunchGame(SelectedGame);
}
}
private void OpenAddGamesModal(bool useGamepad = false)
{
Sound.Play("ui.button.press");
AddGamesModal addGamesPanel = EnsureAddGamesPanel();
if (addGamesPanel == null)
return;
_addGamesModalOpen = true;
StateHasChanged();
addGamesPanel.Open(useGamepad);
}
public void OnAddGamesModalClosed(bool useGamepad)
{
_addGamesModalOpen = false;
if (useGamepad)
_input.ForceGamepadMode();
else
_input.ForceMouseMode();
EnsureAllSoftwareSelectionVisible();
StateHasChanged();
}
private AddGamesModal EnsureAddGamesPanel()
{
if (AddGamesModal.Current != null && AddGamesModal.Current.IsValid())
return AddGamesModal.Current;
AddGamesModal addGamesPanel = Scene.GetAllComponents<AddGamesModal>().FirstOrDefault(panel => panel.IsValid());
if (addGamesPanel != null)
return addGamesPanel;
return AddComponent<AddGamesModal>();
}
private void LaunchGame(GameEntry game)
{
var emulator = EmulatorComponent.Current;
if (!emulator.IsValid())
return;
GamePlayHistory.MarkPlayed(game.Path);
_homeOrderDirty = true;
_suppressHomeCarouselTransitions = true;
_pendingHomeSelectionPath = game.Path;
if (_allSoftwareOpen)
{
_allSoftwareOpen = false;
Sound.Play("ui.button.press");
Hide();
emulator.Restart(game.Path);
return;
}
Sound.Play("ui.button.press");
_suppressHomeCarouselTransitions = true;
_homeCarouselTransitionSuppressionFrames = 4;
Panel?.SkipTransitions();
_homeCarouselRenderVersion++;
_pendingFrozenHomeLaunchPath = game.Path;
_pendingFrozenHomeLaunchFrames = 1;
StateHasChanged();
}
private void CompleteFrozenHomeLaunch()
{
if (string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
return;
var path = _pendingFrozenHomeLaunchPath;
_pendingFrozenHomeLaunchPath = null;
_pendingFrozenHomeLaunchFrames = 0;
var emulator = EmulatorComponent.Current;
if (!emulator.IsValid())
return;
Hide(update: false);
emulator.Restart(path);
}
private void RebuildGameLists(bool preserveSelection = true)
{
bool hadHomeGames = _homeItems.Count > 0;
var selectedGame = preserveSelection ? SelectedGame : null;
bool selectedViewMore = preserveSelection && hadHomeGames && IsHomeViewMoreSelected;
_allGames = [.._games];
_homeItems = [.._games
.OrderByDescending(game => GamePlayHistory.LastPlayedAt(game.Path))
.ThenBy(game => game.DisplayTitle, StringComparer.OrdinalIgnoreCase)];
if (!preserveSelection)
{
_selectedIndex = 0;
}
else if (selectedGame != null && !selectedViewMore)
{
int homeIndex = _homeItems.IndexOf(selectedGame);
if (homeIndex >= 0)
_selectedIndex = homeIndex;
}
else if (selectedViewMore)
{
_selectedIndex = HomeViewMoreIndex;
}
ClampSelectedIndex();
}
private void SelectPendingHomeGame()
{
if (string.IsNullOrWhiteSpace(_pendingHomeSelectionPath))
return;
int homeIndex = _homeItems.FindIndex(game => string.Equals(game.Path, _pendingHomeSelectionPath, StringComparison.OrdinalIgnoreCase));
_selectedIndex = homeIndex >= 0 && homeIndex < HomeGameCount ? homeIndex : HomeViewMoreIndex;
}
private void ClampSelectedIndex()
{
if (HomeEntryCount == 0)
{
_selectedIndex = 0;
}
else
{
_selectedIndex = _selectedIndex.Clamp(0, HomeEntryCount - 1);
}
_allSoftwareSelection = _allSoftwareSelection.Clamp(0, AllSoftwareAddIndex);
EnsureAllSoftwareSelectionVisible();
}
private void SetGamepadMode()
{
_input.ForceGamepadMode();
}
private void SyncVisibilityWithEmulator()
{
var emu = EmulatorComponent.Current;
bool running = emu.IsValid() && emu.IsReady && !string.IsNullOrEmpty(emu.RomPath);
if (running && _isVisible) Hide();
else if (!running && !_isVisible) Show();
}
private SettingsModal EnsureSettingsPanel()
{
if (SettingsModal.Current != null && SettingsModal.Current.IsValid())
return SettingsModal.Current;
SettingsModal settingsPanel = Scene.GetAllComponents<SettingsModal>().FirstOrDefault(panel => panel.IsValid());
if (settingsPanel != null)
return settingsPanel;
return AddComponent<SettingsModal>();
}
private InputHints EnsureInputHintsPanel()
{
if (InputHints.Current != null && InputHints.Current.IsValid())
return InputHints.Current;
InputHints inputHintsPanel = Scene.GetAllComponents<InputHints>().FirstOrDefault(panel => panel.IsValid());
if (inputHintsPanel != null)
return inputHintsPanel;
return AddComponent<InputHints>();
}
private void OpenSettings(bool useGamepad = false)
{
if (useGamepad)
SetGamepadMode();
else
_input.End();
_navFocused = true;
_navSelection = 3;
Sound.Play("ui.button.press");
SettingsModal settingsPanel = EnsureSettingsPanel();
settingsPanel?.Show(useGamepad);
}
public void FocusHeaderAction(int headerSelection, bool useGamepad)
{
_navFocused = true;
_navSelection = headerSelection.Clamp(0, NavItems.Length - 1);
if (useGamepad) _input.ForceGamepadMode();
else _input.End();
StateHasChanged();
}
public void RestoreInputMode(bool useGamepad)
{
if (useGamepad) _input.ForceGamepadMode();
else _input.ForceMouseMode();
StateHasChanged();
}
public void FocusSettingsAction(bool useGamepad)
{
FocusHeaderAction(3, useGamepad);
}
protected override void OnUpdate()
{
SyncVisibilityWithEmulator();
if (!string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
{
if (_pendingFrozenHomeLaunchFrames > 0)
{
_pendingFrozenHomeLaunchFrames--;
return;
}
CompleteFrozenHomeLaunch();
return;
}
if (_homeCarouselSettleRemaining > 0f)
{
_homeCarouselSettleRemaining = MathF.Max(0f, _homeCarouselSettleRemaining - Time.Delta);
if (_homeCarouselSettleRemaining <= 0f && _queuedHomeLaunchGame != null)
{
var queuedLaunch = _queuedHomeLaunchGame;
_queuedHomeLaunchGame = null;
LaunchGame(queuedLaunch);
return;
}
}
if (_isVisible && _suppressHomeCarouselTransitions)
{
if (_homeCarouselTransitionSuppressionFrames > 0)
{
_homeCarouselTransitionSuppressionFrames--;
}
else
{
_suppressHomeCarouselTransitions = false;
StateHasChanged();
}
}
UpdateBackdropTransition();
UpdatePendingTextureDisposals();
UpdateToast();
_pollTimer += Time.Delta;
if (_pollTimer >= PollInterval)
{
_pollTimer = 0f;
var current = GameEntry.GetInstalledPaths();
if (!current.SetEquals(_knownPaths))
{
_knownPaths = current;
_games = GameEntry.Discover();
RebuildGameLists();
QueueArtworkRefresh();
StateHasChanged();
}
}
if (!_isVisible) return;
if (SettingsModal.Current?.IsVisible == true) return;
if (DetailsModal.Current?.IsVisible == true) return;
if (AddGamesModal.Current?.IsVisible == true) return;
if (new Game.Overlay().IsOpen) return;
UpdateAllSoftwareScrollState();
var nav = _input.TickRepeating();
if (nav.Up) { SetGamepadMode(); NavigateUp(); }
if (nav.Down) { SetGamepadMode(); NavigateDown(); }
if (nav.Left) { SetGamepadMode(); NavigateLeft(); }
if (nav.Right) { SetGamepadMode(); NavigateRight(); }
if (Input.Pressed("GBA_A") || Input.Pressed("GBA_Start"))
{
SetGamepadMode();
if (_allSoftwareOpen) ActivateAllSoftwareItem(_allSoftwareSelection);
else if (_navFocused) ActivateNavItem(_navSelection, useGamepad: true);
else ActivateCarouselItem(_selectedIndex);
}
if (Input.Pressed("GBA_B") && _allSoftwareOpen)
{
SetGamepadMode();
CloseAllSoftware();
}
if (Input.Pressed("GBA_B") && _navFocused)
{
SetGamepadMode();
_navFocused = false;
Sound.Play("ui.button.over");
}
if (Input.Pressed("GBA_Select") && CanOpenDetails)
{
SetGamepadMode();
OpenSelectedDetails();
}
}
private void NavigateUp()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareVertical(-1);
return;
}
if (_navFocused)
{
_navFocused = false;
Sound.Play("ui.button.over");
}
}
private void NavigateDown()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareVertical(1);
return;
}
if (!_navFocused)
{
_navFocused = true;
Sound.Play("ui.button.over");
}
}
private void NavigateLeft()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareHorizontal(-1);
return;
}
if (_navFocused)
{
if (_navSelection > 0)
{
_navSelection--;
Sound.Play("ui.button.over");
}
return;
}
SelectCarouselItem(_selectedIndex - 1, useGamepad: true);
}
private void NavigateRight()
{
if (_allSoftwareOpen)
{
NavigateAllSoftwareHorizontal(1);
return;
}
if (_navFocused)
{
if (_navSelection < NavItems.Length - 1)
{
_navSelection++;
Sound.Play("ui.button.over");
}
return;
}
SelectCarouselItem(_selectedIndex + 1, useGamepad: true);
}
private void SelectCarouselItem(int index, bool useGamepad)
{
if (HomeEntryCount == 0)
return;
var next = index.Clamp(0, HomeEntryCount - 1);
if (_selectedIndex == next && !_navFocused)
return;
_selectedIndex = next;
_navFocused = false;
_queuedHomeLaunchGame = null;
_homeCarouselSettleRemaining = HomeCarouselSettleDuration;
if (useGamepad)
SetGamepadMode();
Sound.Play("ui.button.over");
QueueArtworkRefresh();
SyncBackdropTexture();
StateHasChanged();
}
private bool IsHomeViewMoreIndex(int index)
{
return index == HomeViewMoreIndex;
}
private void UpdateBackdropTransition()
{
SyncBackdropTexture();
if (_previousBackdrop == null)
return;
if (_currentBackdrop == null && !_previousBackdropFading)
{
_previousBackdropFading = true;
_backdropVersion++;
StateHasChanged();
return;
}
_previousBackdropAge += Time.Delta;
if (_previousBackdropAge < BackdropRetainDuration)
return;
_previousBackdrop = null;
PruneArtworkTextures();
_backdropVersion++;
StateHasChanged();
}
private void SyncBackdropTexture()
{
var next = SelectedBackdrop;
if (next == _currentBackdrop)
return;
if (_currentBackdrop != null)
_previousBackdrop = _currentBackdrop;
_currentBackdrop = next;
_previousBackdropAge = 0f;
_previousBackdropFading = false;
_backdropVersion++;
}
private void SelectNavItem(int index, bool useGamepad)
{
_navSelection = index.Clamp(0, NavItems.Length - 1);
_navFocused = true;
if (useGamepad)
SetGamepadMode();
else
_input.ForceMouseMode();
StateHasChanged();
}
private void ActivateNavItem(int index, bool useGamepad)
{
SelectNavItem(index, useGamepad);
switch (index)
{
case 0:
ShowUnavailableToast("#toast.netplay.title", "#toast.netplay.message", "group", NavItems[index].ColorClass);
break;
case 1:
ShowUnavailableToast("#toast.news.title", "#toast.news.message", "newspaper", NavItems[index].ColorClass);
break;
case 2:
ShowUnavailableToast("#toast.cloud.title", "#toast.cloud.message", "cloud", NavItems[index].ColorClass);
break;
case 3:
OpenSettings(useGamepad);
break;
case 4:
Sound.Play("ui.button.press");
Game.Close();
break;
}
}
private void ShowUnavailableToast(string title, string message, string icon, string colorClass)
{
_toast.Show(title, message, icon, colorClass);
Sound.Play("ui.button.press");
StateHasChanged();
}
public void ShowToast(string title, string message, string icon = "info", string colorClass = "blue")
{
_toast.Show(title, message, icon, colorClass);
StateHasChanged();
}
private void UpdateToast()
{
if (!_toast.IsRunning)
return;
_toast.Tick(Time.Delta);
StateHasChanged();
}
private void OpenSelectedDetails()
{
if (!CanOpenDetails)
return;
DetailsModal detailsPanel = EnsureDetailsPanel();
detailsPanel?.Open(SelectedGame, _input.UseGamepad);
StateHasChanged();
}
public void RefreshControllerHints()
{
InputHints.Current?.Refresh();
}
private DetailsModal EnsureDetailsPanel()
{
if (DetailsModal.Current != null && DetailsModal.Current.IsValid())
return DetailsModal.Current;
DetailsModal detailsPanel = Scene.GetAllComponents<DetailsModal>().FirstOrDefault(panel => panel.IsValid());
if (detailsPanel != null)
return detailsPanel;
return AddComponent<DetailsModal>();
}
private static bool IsCarouselOffsetMounted(int offset)
{
return offset >= -CarouselMountedRange && offset <= CarouselMountedRange;
}
protected override int BuildHash()
{
if (_isVisible)
return HashCode.Combine(
HashCode.Combine(_selectedIndex, _allSoftwareSelection, (int)AllSoftwareScrollY, _allSoftwareOpen),
HashCode.Combine(AddGamesModalOpen, SettingsModalOpen, _allGames.Count, _homeItems.Count, _suppressHomeCarouselTransitions),
HashCode.Combine(_homeCarouselRenderVersion, _input.UseGamepad, _artworkVersion, _backdropVersion),
_allSoftwareTitleDirection,
HashCode.Combine(_navFocused, _navSelection, _toast.RenderHash));
return HashCode.Combine(_isVisible);
}
protected override void OnDestroy()
{
if (Current == this)
Current = null;
DisposeArtworkTextures();
_currentBackdrop = null;
_previousBackdrop = null;
}
private void DisposeArtworkTextures()
{
var textures = _boxArt.Values.Concat(_logos.Values).Concat(_snaps.Values).Concat(_titles.Values).Concat(_pendingTextureDisposals.Select(item => item.Texture)).Where(texture => texture != null).ToHashSet();
foreach (var texture in textures)
Thumbnails.ReleaseTexture(texture);
_boxArt.Clear();
_logos.Clear();
_snaps.Clear();
_titles.Clear();
_pendingTextureDisposals.Clear();
}
}