using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.VisualTree; using LANCommander.Launcher.Controls; using LANCommander.Launcher.ViewModels; namespace LANCommander.Launcher.Views; public partial class LibraryRowView : UserControl { public static readonly StyledProperty SmallCoverWidthProperty = AvaloniaProperty.Register(nameof(SmallCoverWidth), defaultValue: 105.0); public static readonly StyledProperty SmallCoverHeightProperty = AvaloniaProperty.Register(nameof(SmallCoverHeight), defaultValue: 158.0); public double SmallCoverWidth { get => GetValue(SmallCoverWidthProperty); private set => SetValue(SmallCoverWidthProperty, value); } public double SmallCoverHeight { get => GetValue(SmallCoverHeightProperty); private set => SetValue(SmallCoverHeightProperty, value); } public LibraryRowView() { InitializeComponent(); AddHandler(KeyDownEvent, OnViewKeyDown, RoutingStrategies.Bubble, handledEventsToo: true); } // ── Section navigation helpers ────────────────────────────────────── /// /// Returns the ordered list of visible right-panel sections /// (carousels then grid) for Up/Down navigation. /// private List GetVisibleSections() { var sections = new List(); if (RecentlyPlayedCarousel.IsVisible) sections.Add(RecentlyPlayedCarousel); if (CollectionsCarousel.IsVisible) sections.Add(CollectionsCarousel); sections.Add(LibraryGridItemsRepeater); // always visible return sections; } /// /// Determines which right-panel section currently contains focus. /// private Control? GetFocusedSection(Visual focused) { if (RecentlyPlayedCarousel.IsVisible && RecentlyPlayedCarousel.IsVisualAncestorOf(focused)) return RecentlyPlayedCarousel; if (CollectionsCarousel.IsVisible && CollectionsCarousel.IsVisualAncestorOf(focused)) return CollectionsCarousel; if (LibraryGridItemsRepeater.IsVisualAncestorOf(focused)) return LibraryGridItemsRepeater; return null; } /// /// Focuses the first interactive item inside a section control. /// Scrolls the right panel to the top when targeting the first section. /// private void FocusSectionFirstItem(Control section) { var target = section.GetVisualDescendants() .OfType() .FirstOrDefault(el => el.Focusable && el.IsEffectivelyVisible && el is not ItemsControl && el is not ScrollViewer && el is not TextBox); if (target != null) { target.Focus(NavigationMethod.Directional); var sections = GetVisibleSections(); if (sections.Count > 0 && sections[0] == section) RightPanelScrollViewer.Offset = new Vector(RightPanelScrollViewer.Offset.X, 0); else (target as Control)?.BringIntoView(); } } // ── Key handling ──────────────────────────────────────────────────── private void OnViewKeyDown(object? sender, KeyEventArgs e) { var focusedElement = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Visual; if (focusedElement == null) return; var focusInList = GameListBox == focusedElement || GameListBox.IsVisualAncestorOf(focusedElement); // Right from sidebar: must intercept even when the ListBox's internal // directional navigation has already marked the event as handled. if (e.Key == Key.Right && focusInList) { var sections = GetVisibleSections(); if (sections.Count > 0) { FocusSectionFirstItem(sections[0]); e.Handled = true; } return; } if (e.Handled) return; switch (e.Key) { case Key.Left: // If focus is in the right panel and Left was not consumed by a // child (carousel at index 0, or grid leftmost column), move // focus back to the sidebar list. if (RightPanelScrollViewer.IsVisualAncestorOf(focusedElement)) { FocusListSelectedItem(); e.Handled = true; } break; case Key.Down: // Navigate from current section to the next section below. // For the grid, DirectionalNavigation handles internal Down; // this only fires when the event bubbles (bottom row or carousel). if (RightPanelScrollViewer.IsVisualAncestorOf(focusedElement)) { var sections = GetVisibleSections(); var current = GetFocusedSection(focusedElement); if (current != null) { var idx = sections.IndexOf(current); if (idx >= 0 && idx < sections.Count - 1) { FocusSectionFirstItem(sections[idx + 1]); e.Handled = true; } } } break; case Key.Up: // Navigate from current section to the previous section above. // For the grid, DirectionalNavigation handles internal Up; // this only fires when the event bubbles (top row or carousel). if (RightPanelScrollViewer.IsVisualAncestorOf(focusedElement)) { var sections = GetVisibleSections(); var current = GetFocusedSection(focusedElement); if (current != null) { var idx = sections.IndexOf(current); if (idx > 0) { FocusSectionFirstItem(sections[idx - 1]); e.Handled = true; } } } break; } } private void FocusListSelectedItem() { // Ensure there's a valid selection if (GameListBox.SelectedIndex < 0 && GameListBox.ItemCount > 0) GameListBox.SelectedIndex = 0; if (GameListBox.SelectedIndex >= 0) { // Scroll the selected item into view so its container is realized GameListBox.ScrollIntoView(GameListBox.SelectedIndex); var container = GameListBox.ContainerFromIndex(GameListBox.SelectedIndex); if (container is InputElement input) { input.Focus(NavigationMethod.Directional); return; } } // Fallback: focus the ListBox itself GameListBox.Focus(NavigationMethod.Directional); } // ── Layout ────────────────────────────────────────────────────────── private void RightPanelScrollViewer_SizeChanged(object? sender, SizeChangedEventArgs e) { UpdateGridLayout(e.NewSize.Width); } private void UpdateGridLayout(double availableWidth) { if (LibraryGridItemsRepeater?.Layout is not UniformGridLayout layout) return; const int minCols = 4; const int maxCols = 7; const double coverRatio = 9.0 / 6.0; const double colSpacing = 12.0; const double hPadding = 84.0; // outer StackPanel margin (10+10) + inner StackPanel margin (32+32) const double minWidth = 105.0; double usable = Math.Max(availableWidth - hPadding, minCols * (minWidth + colSpacing)); int cols = (int)Math.Clamp( Math.Floor((usable + colSpacing) / (minWidth + colSpacing)), minCols, maxCols); double coverW = Math.Floor((usable - colSpacing * (cols - 1)) / cols); double coverH = Math.Floor(coverW * coverRatio); SmallCoverWidth = coverW; SmallCoverHeight = coverH; layout.MinItemWidth = coverW; layout.MinItemHeight = coverH; } // ── List interactions ─────────────────────────────────────────────── private void GameList_Tapped(object? sender, TappedEventArgs e) { if (DataContext is LibraryViewModel vm && vm.SelectedGame != null) { vm.ViewGameDetailsCommand.Execute(vm.SelectedGame); } } private void GameList_KeyDown(object? sender, KeyEventArgs e) { if (e.Handled) return; if (e.Key == Key.Return) { if (DataContext is LibraryViewModel vm && vm.SelectedGame != null) { vm.ViewGameDetailsCommand.Execute(vm.SelectedGame); e.Handled = true; } } } }