diff --git a/CHANGELOG.md b/CHANGELOG.md
index d96803c..40ecb38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,30 @@ All notable changes to Aperture Image Viewer are documented here. The format fol
## [Unreleased]
+## [0.8.6-beta1] - 2026-08-21
+
+### Fixed
+- **Folder-tree keyboard** — Tab / Shift+Tab cycle only the folder tree and the tile
+ list (ribbon and the Everything button stay out). Tabbing in does not change the
+ destination selection. Clicking or Tabbing to the tree gives it keyboard focus so
+ arrows move folders immediately. Arrowing the tree commits the selected node at
+ once; the tile pane rebuilds in the background and shows the existing Loading…
+ spinner if that rebuild is slow, instead of gating the next arrow on grid render.
+- **First click** on an unfocused window or pane focuses the clicked folder / tile /
+ canvas in that same click (Explorer-style). The activating click is not swallowed.
+ The folder expand arrow and include checkbox do not change the selected folder.
+
+## [0.8.5-beta1] - 2026-08-21
+
+### Fixed
+- **Tile-pane Up/Down** stay in the same *visual* column at every zoom. 0.8.4
+ stepped by a width-based column count that came out one low once the
+ scrollbar (and padding) were already excluded from the wrap viewport — a
+ 4-column grid walked as 3, so selection moved diagonally. The picker now
+ uses layout X (and a column count that cannot undercut columns already on
+ screen). PageUp/PageDown still move one viewport in that same column;
+ Left/Right are unchanged. Headers are not stops.
+
## [0.8.4-beta1] - 2026-08-21
### Fixed
diff --git a/src/Aperture.App/Aperture.App.csproj b/src/Aperture.App/Aperture.App.csproj
index 874e3f9..616b907 100644
--- a/src/Aperture.App/Aperture.App.csproj
+++ b/src/Aperture.App/Aperture.App.csproj
@@ -16,7 +16,7 @@
trueAssets\aperture.icoAperture
- 0.8.4-beta1
+ 0.8.6-beta1Aperture Image ViewerAperture Image ViewerA fast local image & video browser for Windows.
diff --git a/src/Aperture.App/LibraryPaneFocus.cs b/src/Aperture.App/LibraryPaneFocus.cs
index bbd7ca0..c29b3e2 100644
--- a/src/Aperture.App/LibraryPaneFocus.cs
+++ b/src/Aperture.App/LibraryPaneFocus.cs
@@ -8,9 +8,10 @@
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.
+/// Keyboard focus for the library: folder tree ↔ 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. Tab onto either pane must not change its selection.
///
internal static class LibraryPaneFocus
{
@@ -47,6 +48,127 @@ public static bool ShouldFocusContentsPane(DependencyObject? originalSource, IIn
public static bool ShouldKeepExistingSelection(bool fromOutside, bool newFocusIsItem, bool newFocusIsSelected) =>
fromOutside && newFocusIsItem && !newFocusIsSelected;
+ /// Win32 WM_MOUSEACTIVATE — the click that activates a window.
+ internal const int WmMouseActivate = 0x0021;
+
+ /// Activate and deliver the click to the control (Explorer). Not MA_ACTIVATEANDEAT.
+ internal const int MaActivate = 1;
+
+ ///
+ /// True when this is the click that activates the window. Return
+ /// so the same click focuses the folder / tile /
+ /// canvas — an activation-only first click would swallow it.
+ ///
+ public static bool TryPassActivatingClick(int msg, out IntPtr result)
+ {
+ if (msg != WmMouseActivate)
+ {
+ result = IntPtr.Zero;
+ return false;
+ }
+ result = new IntPtr(MaActivate);
+ return true;
+ }
+
+ internal enum TreeFocusAction
+ {
+ /// Leave WPF's focus target alone (arrows inside the tree, or a click).
+ Leave,
+ /// Focus the already-selected folder so arrows move from there.
+ FocusSelectedItem,
+ ///
+ /// Keep focus on the TreeView — "Everything" is showing; do not select the first folder.
+ ///
+ FocusTree,
+ }
+
+ /// What a left-click on the folder tree should do to keyboard focus.
+ internal enum TreeClickAction
+ {
+ /// Folder row / name / icon — TreeViewItem.Focus() (which also selects).
+ FocusItem,
+ ///
+ /// Expand chevron or include checkbox: do not Focus() the item.
+ /// That call selects the folder; Explorer and 0.8.5 keep those hits off selection.
+ /// Alias text boxes keep the caret.
+ ///
+ Leave,
+ /// Empty tree chrome — focus the TreeView without changing selection.
+ FocusTree,
+ }
+
+ internal enum TreeHit
+ {
+ Row,
+ Expander,
+ IncludeCheckBox,
+ TextInput,
+ Canvas,
+ }
+
+ ///
+ /// Same-click folder focus applies to the row only. The expander
+ /// (, Focusable=False) and the root include
+ /// checkbox must not select the folder.
+ ///
+ public static TreeClickAction OnTreePreviewClick(TreeHit hit) => hit switch
+ {
+ TreeHit.Row => TreeClickAction.FocusItem,
+ TreeHit.Canvas => TreeClickAction.FocusTree,
+ _ => TreeClickAction.Leave,
+ };
+
+ public static TreeClickAction OnTreePreviewClick(DependencyObject? originalSource) =>
+ OnTreePreviewClick(ClassifyTreeHit(originalSource));
+
+ internal static TreeHit ClassifyTreeHit(DependencyObject? originalSource)
+ {
+ var node = originalSource;
+ while (node is not null)
+ {
+ if (node is TextBox or PasswordBox or ComboBox or RichTextBox)
+ return TreeHit.TextInput;
+ // CheckBox before ToggleButton — CheckBox is a ToggleButton.
+ if (node is CheckBox)
+ return TreeHit.IncludeCheckBox;
+ if (node is ToggleButton)
+ return TreeHit.Expander;
+ if (node is TreeViewItem)
+ return TreeHit.Row;
+ if (node is TreeView or Window)
+ return TreeHit.Canvas;
+ node = Parent(node);
+ }
+ return TreeHit.Canvas;
+ }
+
+ ///
+ /// Where keyboard focus should land when it arrives on the folder tree.
+ /// Tab must not change the selected folder; click must not yank focus back
+ /// to a previous node while the new item's IsSelected is still catching up.
+ ///
+ public static TreeFocusAction OnTreeKeyboardArrival(
+ bool fromOutside, bool mousePressed,
+ bool newFocusIsItem, bool newFocusIsSelected,
+ bool hasSelectedItem)
+ {
+ if (!fromOutside)
+ return TreeFocusAction.Leave;
+
+ // Row-click path focuses the item in PreviewMouseDown; IsSelected may still be false.
+ if (mousePressed)
+ return TreeFocusAction.Leave;
+
+ if (newFocusIsItem && newFocusIsSelected)
+ return TreeFocusAction.Leave;
+
+ if (hasSelectedItem)
+ return TreeFocusAction.FocusSelectedItem;
+
+ // Tab onto the tree (or onto the first folder) while Everything is showing.
+ return TreeFocusAction.FocusTree;
+ }
+
public static Hit Classify(DependencyObject? originalSource, IInputElement? currentFocus)
{
// A dialog keeps focus even if the click somehow reached the pane
diff --git a/src/Aperture.App/MainWindow.xaml b/src/Aperture.App/MainWindow.xaml
index c1e8f0a..30ed633 100644
--- a/src/Aperture.App/MainWindow.xaml
+++ b/src/Aperture.App/MainWindow.xaml
@@ -497,12 +497,15 @@
+ Width="120" VerticalAlignment="Center" VerticalContentAlignment="Center" Height="30"
+ IsTabStop="False" />
-
-
+
+
@@ -580,7 +583,8 @@