diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b6427e..bac15df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to Aperture Image Viewer are documented here. The format fol
## [Unreleased]
+## [0.8.3-beta1] - 2026-08-21
+
+### Fixed
+- **Clicking the tile-pane canvas** (empty background, not a tile or section header) now
+ moves keyboard focus to the contents pane, so arrows, PageUp/PageDown, and scroll keys
+ apply to the grid instead of staying on the folder tree.
+
## [0.8.2-beta1] - 2026-08-21
### Added
diff --git a/src/Aperture.App/Aperture.App.csproj b/src/Aperture.App/Aperture.App.csproj
index 7ec9d81..2f98a89 100644
--- a/src/Aperture.App/Aperture.App.csproj
+++ b/src/Aperture.App/Aperture.App.csproj
@@ -16,7 +16,7 @@
true
Assets\aperture.ico
Aperture
- 0.8.2-beta1
+ 0.8.3-beta1
Aperture Image Viewer
Aperture Image Viewer
A fast local image & video browser for Windows.
diff --git a/src/Aperture.App/LibraryPaneFocus.cs b/src/Aperture.App/LibraryPaneFocus.cs
new file mode 100644
index 0000000..6bca3bc
--- /dev/null
+++ b/src/Aperture.App/LibraryPaneFocus.cs
@@ -0,0 +1,111 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using Aperture.App.ViewModels;
+
+namespace Aperture.App;
+
+///
+/// Keyboard focus for the library contents/tile pane. Clicking a tile already
+/// focuses that ; clicking the empty canvas did not,
+/// so arrow / PageUp / PageDown / scroll keys stayed on the folder tree.
+///
+internal static class LibraryPaneFocus
+{
+ internal enum Hit
+ {
+ /// Empty pane background, scrollbar, padding — claim the grid.
+ Canvas,
+ /// A media tile already focuses its .
+ Tile,
+ /// A date-section header already focuses its .
+ SectionHeader,
+ /// Search, rename, notes — leave the caret where it is.
+ TextInput,
+ /// A dialog owns the click or the current focus.
+ Dialog,
+ }
+
+ ///
+ /// True when this click should move keyboard focus onto the contents pane.
+ /// Tile and section-header clicks already focus themselves; text boxes and
+ /// dialogs keep focus.
+ ///
+ public static bool ShouldFocusContentsPane(Hit hit) => hit is Hit.Canvas;
+
+ public static bool ShouldFocusContentsPane(DependencyObject? originalSource, IInputElement? currentFocus) =>
+ ShouldFocusContentsPane(Classify(originalSource, currentFocus));
+
+ public static Hit Classify(DependencyObject? originalSource, IInputElement? currentFocus)
+ {
+ // A dialog keeps focus even if the click somehow reached the pane
+ // (modeless / leftover hit-test). Modal dialogs never get here.
+ if (IsInDialog(originalSource) || IsInDialog(currentFocus as DependencyObject))
+ return Hit.Dialog;
+
+ if (IsTextInput(originalSource))
+ return Hit.TextInput;
+
+ if (IsTile(originalSource))
+ return Hit.Tile;
+
+ if (IsSectionHeader(originalSource))
+ return Hit.SectionHeader;
+
+ return Hit.Canvas;
+ }
+
+ private static bool IsInDialog(DependencyObject? node)
+ {
+ if (node is null)
+ return false;
+ var window = Window.GetWindow(node);
+ return window is not null and not MainWindow;
+ }
+
+ private static bool IsTextInput(DependencyObject? node)
+ {
+ while (node is not null)
+ {
+ if (node is TextBox or PasswordBox or ComboBox or RichTextBox)
+ return true;
+ if (node is ListBoxItem or ListBox or Window)
+ break;
+ node = Parent(node);
+ }
+ return false;
+ }
+
+ private static bool IsTile(DependencyObject? node)
+ {
+ while (node is not null)
+ {
+ if (node is ListBoxItem { DataContext: TileVm })
+ return true;
+ if (node is ListBox or Window)
+ break;
+ node = Parent(node);
+ }
+ return false;
+ }
+
+ private static bool IsSectionHeader(DependencyObject? node)
+ {
+ while (node is not null)
+ {
+ if (node is ToggleButton)
+ return true;
+ if (node is ListBoxItem or ListBox or Window)
+ break;
+ node = Parent(node);
+ }
+ return false;
+ }
+
+ private static DependencyObject? Parent(DependencyObject node) =>
+ node is Visual or System.Windows.Media.Media3D.Visual3D
+ ? VisualTreeHelper.GetParent(node)
+ : LogicalTreeHelper.GetParent(node);
+}
diff --git a/src/Aperture.App/MainWindow.xaml b/src/Aperture.App/MainWindow.xaml
index fd4c9a3..4eb60bf 100644
--- a/src/Aperture.App/MainWindow.xaml
+++ b/src/Aperture.App/MainWindow.xaml
@@ -753,12 +753,16 @@
-
-
+
+
-
+
diff --git a/src/Aperture.App/MainWindow.xaml.cs b/src/Aperture.App/MainWindow.xaml.cs
index 0419d34..5c73b8c 100644
--- a/src/Aperture.App/MainWindow.xaml.cs
+++ b/src/Aperture.App/MainWindow.xaml.cs
@@ -661,6 +661,19 @@ private void OnTileBadgeClick(object sender, RoutedEventArgs e)
}
}
+ ///
+ /// Empty-canvas click in the tile pane: move keyboard focus onto the grid so
+ /// arrows / PageUp / PageDown / scroll keys apply here, not the folder tree.
+ /// Tile and section-header clicks already focus themselves; text boxes and
+ /// dialogs are left alone.
+ ///
+ private void OnContentsPanePreviewLeftDown(object sender, MouseButtonEventArgs e)
+ {
+ if (LibraryPaneFocus.ShouldFocusContentsPane(
+ e.OriginalSource as DependencyObject, Keyboard.FocusedElement))
+ ItemsList.Focus();
+ }
+
// --- Drag a tile (or the whole selection) out to the shell as real file(s) ---
// Lets you drop images straight onto an email, a folder, or any file-drop target.
private Point _tileDragStart;
diff --git a/tests/Aperture.Core.Tests/LibraryPaneFocusTests.cs b/tests/Aperture.Core.Tests/LibraryPaneFocusTests.cs
new file mode 100644
index 0000000..46ac10e
--- /dev/null
+++ b/tests/Aperture.Core.Tests/LibraryPaneFocusTests.cs
@@ -0,0 +1,38 @@
+using Aperture.App;
+
+namespace Aperture.Core.Tests;
+
+///
+/// Contract for when a contents-pane click should move keyboard focus onto the
+/// tile grid. Live WPF focus (dispatcher, visual tree, Keyboard.Focus) is not
+/// exercised here — verify the click path on a real window:
+/// folder in the tree → click empty tile-pane canvas → arrows / PageUp /
+/// PageDown / scroll keys move the grid, not the tree. Clicking a folder,
+/// a tile, a section header, the search box, or a dialog must stay as today.
+///
+public class LibraryPaneFocusTests
+{
+ [Fact]
+ public void Canvas_ClaimsTheGrid() =>
+ Assert.True(LibraryPaneFocus.ShouldFocusContentsPane(LibraryPaneFocus.Hit.Canvas));
+
+ [Fact]
+ public void Tile_DoesNotClaim() =>
+ Assert.False(LibraryPaneFocus.ShouldFocusContentsPane(LibraryPaneFocus.Hit.Tile));
+
+ [Fact]
+ public void SectionHeader_DoesNotClaim() =>
+ Assert.False(LibraryPaneFocus.ShouldFocusContentsPane(LibraryPaneFocus.Hit.SectionHeader));
+
+ [Fact]
+ public void TextInput_DoesNotClaim() =>
+ Assert.False(LibraryPaneFocus.ShouldFocusContentsPane(LibraryPaneFocus.Hit.TextInput));
+
+ [Fact]
+ public void Dialog_DoesNotClaim() =>
+ Assert.False(LibraryPaneFocus.ShouldFocusContentsPane(LibraryPaneFocus.Hit.Dialog));
+
+ [Fact]
+ public void Classify_NullSourceIsCanvas() =>
+ Assert.Equal(LibraryPaneFocus.Hit.Canvas, LibraryPaneFocus.Classify(null, null));
+}