diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4a7cc71..2ca86a7 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -4,6 +4,9 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
## [Unreleased]
+### Fixes
+- Session menu: ← and → move across the open-window cards by position without failing; nothing moves past the last card of a row. (#133)
+
## [1.6.0-alpha.5]
### Fixes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5074c8d..00dace5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
## [Unreleased]
+### Correções
+- Menu da sessão: ← e → percorrem os cartões das janelas abertas pela posição, sem falhar; no último cartão da linha nada se move. (#133)
+
## [1.6.0-alpha.5]
### Correções
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 6918d61..fc0e594 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -331,6 +331,7 @@ Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (te
- [ ] Durante o jogo, Share + △ (Select + Y) abre o overlay; → em uma opção lateral leva ao primeiro cartão de janela e ✕ ativa a janela escolhida. Resultado: ______
- [ ] No primeiro cartão de uma coluna, ← retorna à opção lateral de origem; ↑/↓ e ←/→ navegam pelos demais cartões, inclusive com rolagem. Resultado: ______
- [ ] Com seletor de sessão ou confirmação de fechamento aberto, as direções ficam dentro do diálogo. Sem janelas abertas, → não perde o foco. Resultado: ______
+- [ ] Com **3 ou mais janelas** abertas (uma linha de cartões): → e ← percorrem a linha cartão a cartão; → no último cartão não faz nada (não pula para outro lugar); ← no primeiro volta à opção lateral de origem. Com várias linhas: → e ← ficam na linha do cartão. Resultado: ______
- [ ] Os cartões não exibem o botão X no canto. Quadrado/Delete continuam pedindo confirmação para fechar a janela selecionada. Resultado: ______
- [ ] A logo fornecida do ControlFS aparece no painel fixo do overlay, tanto instalado quanto ausente, inclusive no aplicativo publicado. Resultado: ______
- [ ] Com overlay aberto ou outro aplicativo em primeiro plano, a tela Console ao fundo não reage ao DualShock. Resultado: ______
diff --git a/src/ConsoleMode/SessionMenuWindow.xaml.cs b/src/ConsoleMode/SessionMenuWindow.xaml.cs
index be3ca35..d5439dd 100644
--- a/src/ConsoleMode/SessionMenuWindow.xaml.cs
+++ b/src/ConsoleMode/SessionMenuWindow.xaml.cs
@@ -93,6 +93,13 @@ public SessionMenuWindow(MainViewModel viewModel, ScreenRect? target)
UiSounds.Play(UiSound.Move);
return true;
}
+ // Along a row of window cards: pick the neighbour by position, not by the spatial search.
+ if (!_editingVolume && direction is (FocusNavigationDirection.Left or FocusNavigationDirection.Right)
+ && !ViewModel.IsSessionPickerOpen && MoveAcrossWindowCards(direction))
+ {
+ UiSounds.Play(UiSound.Move);
+ return true;
+ }
// Cross into the grid explicitly; spatial focus may prefer another sidebar row.
if (!_editingVolume && direction == FocusNavigationDirection.Right && !ViewModel.IsSessionPickerOpen
&& Root.XamlRoot is { } root
@@ -476,6 +483,48 @@ private bool FocusFirstWindowCard()
return false;
}
+ ///
+ /// Left/right between window cards by position: the card on the same row with the next X to that side.
+ /// The XY focus search can miss the neighbour (cards scaled while focused, clipped or scrolled in the
+ /// panel), which left the grid impossible to cross sideways. The end of a row is an end: Right on the last
+ /// card is consumed so the search can't jump somewhere else; Left on the first column is the caller's
+ /// (back to the side panel). False when the focus is not on a window card.
+ ///
+ private bool MoveAcrossWindowCards(FocusNavigationDirection direction)
+ {
+ if (Root.XamlRoot is not { } root
+ || FocusManager.GetFocusedElement(root) is not FrameworkElement { DataContext: SwitchWindowItem } current)
+ return false;
+
+ Windows.Foundation.Point Origin(FrameworkElement e) => e.TransformToVisual(SwitcherList).TransformPoint(new Windows.Foundation.Point(0, 0));
+ var from = Origin(current);
+ var rowTolerance = Math.Max(current.ActualHeight / 2, 1);
+
+ Control? best = null;
+ var bestX = 0d;
+ for (var i = 0; i < ViewModel.SwitcherWindows.Count; i++)
+ {
+ if (SwitcherList.ContainerFromIndex(i) is not { } container
+ || FocusManager.FindFirstFocusableElement(container) is not Control card
+ || ReferenceEquals(card, current))
+ continue;
+ var at = Origin(card);
+ if (Math.Abs(at.Y - from.Y) > rowTolerance) continue;
+ var dx = at.X - from.X;
+ if (direction == FocusNavigationDirection.Right ? dx <= 1 : dx >= -1) continue;
+ if (best is null || (direction == FocusNavigationDirection.Right ? at.X < bestX : at.X > bestX))
+ {
+ best = card;
+ bestX = at.X;
+ }
+ }
+
+ if (best is null) return direction == FocusNavigationDirection.Right;
+ best.Focus(FocusState.Keyboard);
+ best.StartBringIntoView();
+ return true;
+ }
+
/// True when the focus is on a window card with no card to its left (the first column of the grid).
private bool IsOnLeftmostCard()
{