diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json
index 0499ffa8f..a6f885972 100644
--- a/src/Languages/lang_en.json
+++ b/src/Languages/lang_en.json
@@ -1,4 +1,8 @@
{
+ "Navigation menu:": "Navigation menu:",
+ "Automatic": "Automatic",
+ "Docked open": "Docked open",
+ "Sliding overlay": "Sliding overlay",
"{0} of {1} operations completed": "{0} of {1} operations completed",
"{0} is now {1}": "{0} is now {1}",
"Enabled": "Enabled",
diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
index a733c9d1b..69727307b 100644
--- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
+++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
@@ -24,18 +24,6 @@ public partial class SidebarViewModel : ViewModelBase
partial void OnUpdatesBadgeCountChanged(int value) =>
UpdatesBadgeVisible = value > 0;
- partial void OnUpdatesBadgeVisibleChanged(bool value)
- {
- OnPropertyChanged(nameof(UpdatesBadgeExpandedVisible));
- OnPropertyChanged(nameof(UpdatesBadgeCompactVisible));
- }
-
- partial void OnBundlesBadgeVisibleChanged(bool value)
- {
- OnPropertyChanged(nameof(BundlesBadgeExpandedVisible));
- OnPropertyChanged(nameof(BundlesBadgeCompactVisible));
- }
-
// ─── Loading indicators ───────────────────────────────────────────────────
[ObservableProperty]
private bool _discoverIsLoading;
@@ -47,31 +35,84 @@ partial void OnBundlesBadgeVisibleChanged(bool value)
private bool _installedIsLoading;
// ─── Pane open/closed ─────────────────────────────────────────────────────
- // Starts collapsed: the pane is a floating overlay in every size class (see MainWindow's
- // adaptive logic), so there is no persistent open-on-launch state.
+ // NavMenuMode: Automatic mirrors WinUI (dock ≥1600px, else rail+overlay), Docked = always
+ // inline, Overlay = always rail+overlay. IsPaneOpen = labeled pane showing, in any mode.
+ public enum NavMode { Automatic, Docked, Overlay }
+
+ private const double ExpandedThreshold = 1600; // WinUI ExpandedModeThresholdWidth
+ private const double CompactThreshold = 800; // WinUI CompactModeThresholdWidth
+
+ // Live window width, pushed by MainWindow's bounds observer.
[ObservableProperty]
- private bool isPaneOpen;
+ private double _windowWidth = 1450;
+
+ [ObservableProperty]
+ private NavMode _mode = ParseMode(Settings.GetValue(Settings.K.NavMenuMode));
+
+ [ObservableProperty]
+ private bool _isPaneOpen;
+
+ public static NavMode ParseMode(string value) => value switch
+ {
+ "docked" => NavMode.Docked,
+ "overlay" => NavMode.Overlay,
+ _ => NavMode.Automatic,
+ };
+
+ // Whether the pane docks inline (vs. the rail + sliding overlay).
+ public bool Docked => Mode switch
+ {
+ NavMode.Docked => true,
+ NavMode.Overlay => false,
+ _ => WindowWidth >= ExpandedThreshold,
+ };
+
+ private bool RailAllowed => WindowWidth >= CompactThreshold;
+
+ // Icon rail vs docked labeled pane vs sliding overlay — mutually exclusive.
+ public bool NavDockVisible => Docked && IsPaneOpen;
+ public bool RailVisible => (Docked && !IsPaneOpen) || (!Docked && RailAllowed);
+ public bool OverlayActive => !Docked && IsPaneOpen;
+
+ private bool _wasDocked;
+
+ public SidebarViewModel()
+ {
+ _wasDocked = Docked;
+ _isPaneOpen = Docked && !Settings.Get(Settings.K.CollapseNavMenuOnWideScreen);
+ }
+
+ partial void OnWindowWidthChanged(double value) => ReconcileDock();
- // Only persist the open/closed choice as the "collapse on wide screen" preference
- // while the pane is inline (Expanded mode). In the overlay modes used on smaller
- // windows, opening/closing is transient and must not overwrite the saved preference.
- // The MainWindow's adaptive logic keeps this in sync with the SplitView display mode.
- public bool PersistPaneCollapsePreference { get; set; } = true;
+ partial void OnModeChanged(NavMode value) => ReconcileDock();
+
+ // On a docked-state flip, open the pane by default (unless collapsed on a wide screen), or
+ // collapse when leaving docked mode. Mirrors WinUI's startup IsPaneOpen logic.
+ private void ReconcileDock()
+ {
+ bool docked = Docked;
+ if (docked != _wasDocked)
+ {
+ _wasDocked = docked;
+ IsPaneOpen = docked && !Settings.Get(Settings.K.CollapseNavMenuOnWideScreen);
+ }
+ RaiseLayoutChanged();
+ }
partial void OnIsPaneOpenChanged(bool value)
{
- if (PersistPaneCollapsePreference)
+ // Persist the collapse choice only while docked (WinUI persisted only at ≥1600px).
+ if (Docked)
Settings.Set(Settings.K.CollapseNavMenuOnWideScreen, !value);
- OnPropertyChanged(nameof(UpdatesBadgeExpandedVisible));
- OnPropertyChanged(nameof(UpdatesBadgeCompactVisible));
- OnPropertyChanged(nameof(BundlesBadgeExpandedVisible));
- OnPropertyChanged(nameof(BundlesBadgeCompactVisible));
+ RaiseLayoutChanged();
}
- public bool UpdatesBadgeExpandedVisible => UpdatesBadgeVisible && IsPaneOpen;
- public bool UpdatesBadgeCompactVisible => UpdatesBadgeVisible && !IsPaneOpen;
- public bool BundlesBadgeExpandedVisible => BundlesBadgeVisible && IsPaneOpen;
- public bool BundlesBadgeCompactVisible => BundlesBadgeVisible && !IsPaneOpen;
+ private void RaiseLayoutChanged()
+ {
+ OnPropertyChanged(nameof(NavDockVisible));
+ OnPropertyChanged(nameof(RailVisible));
+ OnPropertyChanged(nameof(OverlayActive));
+ }
// ─── Selected page ────────────────────────────────────────────────────────
[ObservableProperty]
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
index ceed9ffd3..137c60fc9 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
@@ -61,11 +61,12 @@
Background="{DynamicResource MicaPageBackground}"
ColumnDefinitions="Auto,*">
-
+
+
+
+
+
+
+
+
+
+
@@ -338,7 +360,7 @@
Grid.Column="0" Grid.ColumnSpan="2"
Background="Transparent"
ZIndex="9"
- IsVisible="{Binding Sidebar.IsPaneOpen}"
+ IsVisible="{Binding Sidebar.OverlayActive}"
automation:AutomationProperties.AccessibilityView="Raw"
PointerPressed="FlyoutDismiss_PointerPressed"/>
@@ -350,8 +372,8 @@
VerticalAlignment="Stretch"
ZIndex="10"
Background="{DynamicResource AppWindowBackground}"
- IsHitTestVisible="{Binding Sidebar.IsPaneOpen}"
- Classes.open="{Binding Sidebar.IsPaneOpen}">
+ IsHitTestVisible="{Binding Sidebar.OverlayActive}"
+ Classes.open="{Binding Sidebar.OverlayActive}">
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
index 6b19f3ec5..8d80005f7 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
@@ -259,7 +259,9 @@ private void Window_KeyDown(object? sender, KeyEventArgs e)
private void OnCurrentPageChanged(object? sender, PageType pageType)
{
// Like WinUI's NavigationView: picking a page collapses the sliding flyout back to the rail.
- ViewModel.Sidebar.IsPaneOpen = false;
+ // A docked pane stays put (it's a persistent inline pane, not a transient overlay).
+ if (!ViewModel.Sidebar.Docked)
+ ViewModel.Sidebar.IsPaneOpen = false;
if (!_focusSidebarSelectionOnNextPageChange)
return;
@@ -305,17 +307,21 @@ private void UpdateOperationsPanelRow()
}
}
- // ─── Navigation rail (responsive) ─────────────────────────────────────────
- // The always-visible icon rail shows on roomy windows and collapses below 800px so narrow
- // windows give the content full width (the hamburger + sliding flyout still provide nav).
+ // ─── Navigation rail / docked pane (responsive) ────────────────────────────
+ // Feed the live window width to the sidebar, which resolves the layout mode and drives
+ // the NavRail / NavDock / overlay visibility bindings.
private void SetupResponsiveRail()
=> MainContentRoot.GetObservable(BoundsProperty)
.SubscribeValue(b =>
{
if (b.Width <= 0) return;
- NavRail.IsVisible = b.Width >= 800;
+ ViewModel.Sidebar.WindowWidth = b.Width;
});
+ // Re-reads the NavMenuMode setting so the layout switch applies live from the settings page.
+ public void RefreshNavigationMode()
+ => ViewModel.Sidebar.Mode = SidebarViewModel.ParseMode(Settings.GetValue(Settings.K.NavMenuMode));
+
// Light-dismiss: clicking outside the open flyout closes it (no darkening — the layer is transparent).
private void FlyoutDismiss_PointerPressed(object? sender, PointerPressedEventArgs e)
=> ViewModel.Sidebar.IsPaneOpen = false;
diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml
index 3b56a5789..f7a2463ed 100644
--- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml
+++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml
@@ -23,9 +23,13 @@
CornerRadius="8,8,0,0"/>
+
+
MainWindow.Instance?.RefreshNavigationMode();
}
}
diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
index 5a55f4341..ecdf77673 100644
--- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
+++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
@@ -38,6 +38,7 @@ public enum K
AlreadyWarnedAboutChocolateyMigration,
ShownTelemetryBanner,
CollapseNavMenuOnWideScreen,
+ NavMenuMode,
EnablePackageBackup_LOCAL,
EnablePackageBackup_CLOUD,
ChangeBackupOutputDirectory,
@@ -147,6 +148,7 @@ public static string ResolveKey(K key)
K.AlreadyWarnedAboutChocolateyMigration => "AlreadyWarnedAboutChocolateyMigration",
K.ShownTelemetryBanner => "ShownTelemetryBanner",
K.CollapseNavMenuOnWideScreen => "CollapseNavMenuOnWideScreen",
+ K.NavMenuMode => "NavMenuMode",
K.EnablePackageBackup_LOCAL => "EnablePackageBackup",
K.EnablePackageBackup_CLOUD => "EnablePackageBackup_CLOUD",
K.ChangeBackupOutputDirectory => "ChangeBackupOutputDirectory",