From 4a324b475dea2c6687924ed3e1a4ca4d77c48169 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 8 Jul 2026 13:49:57 -0400 Subject: [PATCH] Restore dockable navigation pane with adaptive/docked/overlay modes --- src/Languages/lang_en.json | 4 + .../ViewModels/SidebarViewModel.cs | 99 +++++++++++++------ src/UniGetUI.Avalonia/Views/MainWindow.axaml | 30 +++++- .../Views/MainWindow.axaml.cs | 16 ++- .../Pages/SettingsPages/Interface_P.axaml | 6 +- .../Pages/SettingsPages/Interface_P.axaml.cs | 8 ++ .../SettingsEngine_Names.cs | 2 + 7 files changed, 126 insertions(+), 39 deletions(-) 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",