From faa991812e655f7c78192dbaff07b0e4ad6e261a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 08:39:40 +0200 Subject: [PATCH 1/9] feat: sign out enabled, expand/collapse sequenced, animation improvements --- App/Controls/ExpandContent.xaml | 69 +++++++------- App/Controls/ExpandContent.xaml.cs | 89 ++++++++++++++++--- App/Services/RpcController.cs | 5 ++ App/ViewModels/AgentViewModel.cs | 12 ++- .../TrayWindowLoginRequiredViewModel.cs | 7 ++ App/ViewModels/TrayWindowViewModel.cs | 11 +-- .../Pages/TrayWindowLoginRequiredPage.xaml | 9 ++ App/Views/Pages/TrayWindowMainPage.xaml | 1 - App/Views/TrayWindow.xaml | 6 ++ App/Views/TrayWindow.xaml.cs | 75 +++++++++++++--- 10 files changed, 216 insertions(+), 68 deletions(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index d36170d..f2b9840 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -1,50 +1,53 @@ - - + xmlns:toolkit="using:CommunityToolkit.WinUI"> + + + - - - - + + + + - - - + + + + - - - - + + + + + diff --git a/App/Controls/ExpandContent.xaml.cs b/App/Controls/ExpandContent.xaml.cs index 1cd5d2f..4b99043 100644 --- a/App/Controls/ExpandContent.xaml.cs +++ b/App/Controls/ExpandContent.xaml.cs @@ -2,38 +2,101 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Markup; +using System; +using System.Threading; +using System.Threading.Tasks; namespace Coder.Desktop.App.Controls; + [ContentProperty(Name = nameof(Children))] [DependencyProperty("IsOpen", DefaultValue = false)] public sealed partial class ExpandContent : UserControl { public UIElementCollection Children => CollapsiblePanel.Children; + private bool? _pendingIsOpen; + + private static readonly SemaphoreSlim _sem = new(1, 1); + + public ExpandContent() { InitializeComponent(); + Loaded += (_, __) => + { + if (_pendingIsOpen is bool v) + { + _ = AnimateAsync(v); + _pendingIsOpen = null; + } + }; } - public void CollapseAnimation_Completed(object? sender, object args) + partial void OnIsOpenChanged(bool oldValue, bool newValue) { - // Hide the panel completely when the collapse animation is done. This - // cannot be done with keyframes for some reason. - // - // Without this, the space will still be reserved for the panel. - CollapsiblePanel.Visibility = Visibility.Collapsed; + if (!this.IsLoaded) + { + _pendingIsOpen = newValue; + return; + } + _ = AnimateAsync(newValue); } - partial void OnIsOpenChanged(bool oldValue, bool newValue) + private async Task AnimateAsync(bool open) + { + await _sem.WaitAsync(); + + try + { + if (open) + { + if (_currentlyOpen is not null && _currentlyOpen != this) + await _currentlyOpen.StartCollapseAsync(); + + _currentlyOpen = this; + CollapsiblePanel.Visibility = Visibility.Visible; + + VisualStateManager.GoToState(this, "ExpandedState", true); + await ExpandAsync(); // wait for your own expand + } + else + { + if (_currentlyOpen == this) _currentlyOpen = null; + await StartCollapseAsync(); + } + } + finally + { + _sem.Release(); + } + } + + private static ExpandContent? _currentlyOpen; + private TaskCompletionSource? _collapseTcs; + + private async Task ExpandAsync() { - var newState = newValue ? "ExpandedState" : "CollapsedState"; + CollapsiblePanel.Visibility = Visibility.Visible; + VisualStateManager.GoToState(this, "ExpandedState", true); + + var tcs = new TaskCompletionSource(); + void done(object? s, object e) { ExpandSb.Completed -= done; tcs.SetResult(); } + ExpandSb.Completed += done; + await tcs.Task; + } - // The animation can't set visibility when starting or ending the - // animation. - if (newValue) - CollapsiblePanel.Visibility = Visibility.Visible; + private Task StartCollapseAsync() + { + _collapseTcs = new TaskCompletionSource(); + VisualStateManager.GoToState(this, "CollapsedState", true); + return _collapseTcs.Task; + } - VisualStateManager.GoToState(this, newState, true); + private void CollapseStoryboard_Completed(object sender, object e) + { + CollapsiblePanel.Visibility = Visibility.Collapsed; + _collapseTcs?.TrySetResult(); + _collapseTcs = null; } } diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index 70dfe9f..ca5ef7b 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -234,6 +234,11 @@ public async Task StopVpn(CancellationToken ct = default) MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}"); } + + if (reply.Stop.Success) + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); + } } public async ValueTask DisposeAsync() diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 34b01d7..cd5907b 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -237,12 +237,20 @@ public AgentViewModel(ILogger logger, ICoderApiClientFactory cod Id = id; - PropertyChanged += (_, args) => + PropertyChanging += (x, args) => { if (args.PropertyName == nameof(IsExpanded)) { - _expanderHost.HandleAgentExpanded(Id, IsExpanded); + var value = !IsExpanded; + if (value) + _expanderHost.HandleAgentExpanded(Id, value); + } + }; + PropertyChanged += (x, args) => + { + if (args.PropertyName == nameof(IsExpanded)) + { // Every time the drawer is expanded, re-fetch all apps. if (IsExpanded && !FetchingApps) FetchApps(); diff --git a/App/ViewModels/TrayWindowLoginRequiredViewModel.cs b/App/ViewModels/TrayWindowLoginRequiredViewModel.cs index 628be72..abc1257 100644 --- a/App/ViewModels/TrayWindowLoginRequiredViewModel.cs +++ b/App/ViewModels/TrayWindowLoginRequiredViewModel.cs @@ -2,6 +2,7 @@ using Coder.Desktop.App.Views; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; namespace Coder.Desktop.App.ViewModels; @@ -31,4 +32,10 @@ public void Login() _signInWindow.Closed += (_, _) => _signInWindow = null; _signInWindow.Activate(); } + + [RelayCommand] + public void Exit() + { + _ = ((App)Application.Current).ExitApplication(); + } } diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index cfa5163..71ee9cc 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -126,7 +126,7 @@ public void HandleAgentExpanded(Uuid id, bool expanded) if (!expanded) return; _hasExpandedAgent = true; // Collapse every other agent. - foreach (var otherAgent in Agents.Where(a => a.Id != id)) + foreach (var otherAgent in Agents.Where(a => a.Id != id && a.IsExpanded == true)) otherAgent.SetExpanded(false); } @@ -360,11 +360,12 @@ private void ShowFileSyncListWindow() } [RelayCommand] - private void SignOut() + private async Task SignOut() { - if (VpnLifecycle is not VpnLifecycle.Stopped) - return; - _credentialManager.ClearCredentials(); + //if (VpnLifecycle is not VpnLifecycle.Stopped) + // return; + await _rpcController.StopVpn(); + await _credentialManager.ClearCredentials(); } [RelayCommand] diff --git a/App/Views/Pages/TrayWindowLoginRequiredPage.xaml b/App/Views/Pages/TrayWindowLoginRequiredPage.xaml index ce161e3..c1d69aa 100644 --- a/App/Views/Pages/TrayWindowLoginRequiredPage.xaml +++ b/App/Views/Pages/TrayWindowLoginRequiredPage.xaml @@ -34,5 +34,14 @@ + + + + + diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index f3549c2..283867d 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -333,7 +333,6 @@ diff --git a/App/Views/TrayWindow.xaml b/App/Views/TrayWindow.xaml index 0d87874..7ceba9d 100644 --- a/App/Views/TrayWindow.xaml +++ b/App/Views/TrayWindow.xaml @@ -20,5 +20,11 @@ + + + diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 5d1755c..7bc7b3c 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -1,8 +1,3 @@ -using System; -using System.Runtime.InteropServices; -using Windows.Graphics; -using Windows.System; -using Windows.UI.Core; using Coder.Desktop.App.Controls; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; @@ -15,6 +10,12 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Animation; +using System; +using System.Runtime.InteropServices; +using Windows.Graphics; +using Windows.System; +using Windows.UI.Core; using WinRT.Interop; using WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs; @@ -24,6 +25,14 @@ public sealed partial class TrayWindow : Window { private const int WIDTH = 300; + private readonly AppWindow _aw; + + public double ProxyHeight { get; private set; } + + // This is used to know the "start point of the animation" + private int _lastWindowHeight; + private bool _resizeInProgress; + private NativeApi.POINT? _lastActivatePosition; private int _maxHeightSinceLastActivation; @@ -82,8 +91,34 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan var value = 2; // Best effort. This does not work on Windows 10. _ = NativeApi.DwmSetWindowAttribute(windowHandle, 33, ref value, Marshal.SizeOf()); + + _aw = AppWindow.GetFromWindowId( + Win32Interop.GetWindowIdFromWindow( + WinRT.Interop.WindowNative.GetWindowHandle(this))); + SizeProxy.Height = 0; + SizeProxy.SizeChanged += (_, e) => + { + if (!_resizeInProgress) return; + + var newHeight = (int)Math.Round( + e.NewSize.Height * DisplayScale.WindowScale(this)); + + var delta = newHeight - _lastWindowHeight; + if (delta == 0) return; + + var pos = AppWindow.Position; + var size = AppWindow.Size; + + // Shift upward when height increases + pos.Y -= delta; + size.Height = newHeight; + + AppWindow.MoveAndResize(new RectInt32(pos.X, pos.Y, size.Width, size.Height)); + _lastWindowHeight = newHeight; + }; } + private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, SyncSessionControllerStateModel syncSessionModel) { @@ -140,7 +175,27 @@ public void SetRootFrame(Page page) private void RootFrame_SizeChanged(object sender, SizedFrameEventArgs e) { - MoveAndResize(e.NewSize.Height); + AnimateWindowHeight(e.NewSize.Height); + } + + private void AnimateWindowHeight(double targetHeight) + { + // Remember where we start + _lastWindowHeight = AppWindow.Size.Height; + _resizeInProgress = true; + + var anim = new DoubleAnimation + { + To = targetHeight, + Duration = TimeSpan.FromMilliseconds(200), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }, + EnableDependentAnimation = true + }; + + Storyboard.SetTarget(anim, SizeProxy); + Storyboard.SetTargetProperty(anim, "Height"); + + new Storyboard { Children = { anim } }.Begin(); } private void MoveAndResize(double height) @@ -190,14 +245,6 @@ private PointInt32 CalculateWindowPosition(SizeInt32 size) { var width = size.Width; var height = size.Height; - // For positioning purposes, pretend the window is the maximum size it - // has been since it was last activated. This has the affect of - // allowing the window to move up to accomodate more content, but - // prevents it from moving back down when the window shrinks again. - // - // Prevents a lot of jittery behavior with app drawers. - if (height < _maxHeightSinceLastActivation) - height = _maxHeightSinceLastActivation; var cursorPosition = _lastActivatePosition; if (cursorPosition is null) From 0973c9ef68b737f37b4de01c7f5d49232b0615ee Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 14:57:04 +0200 Subject: [PATCH 2/9] added a delay on the animation to remove the breathing animation --- App/Controls/ExpandContent.xaml | 16 +++++++-------- App/Controls/ExpandContent.xaml.cs | 31 ++++++++++-------------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index f2b9840..788264a 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -22,14 +22,14 @@ + Storyboard.TargetProperty="Opacity" BeginTime="0:0:0.16" + To="1" Duration="0:0:0.16"/> + Storyboard.TargetProperty="Y" BeginTime="0:0:0.16" + To="0" Duration="0:0:0.16"/> @@ -37,14 +37,14 @@ Completed="{x:Bind CollapseStoryboard_Completed}"> + To="0" Duration="0:0:0.16"/> + To="-16" Duration="0:0:0.16"/> diff --git a/App/Controls/ExpandContent.xaml.cs b/App/Controls/ExpandContent.xaml.cs index 4b99043..2e9bcb3 100644 --- a/App/Controls/ExpandContent.xaml.cs +++ b/App/Controls/ExpandContent.xaml.cs @@ -17,8 +17,6 @@ public sealed partial class ExpandContent : UserControl private bool? _pendingIsOpen; - private static readonly SemaphoreSlim _sem = new(1, 1); - public ExpandContent() { @@ -45,30 +43,21 @@ partial void OnIsOpenChanged(bool oldValue, bool newValue) private async Task AnimateAsync(bool open) { - await _sem.WaitAsync(); - - try + if (open) { - if (open) - { - if (_currentlyOpen is not null && _currentlyOpen != this) - await _currentlyOpen.StartCollapseAsync(); + if (_currentlyOpen is not null && _currentlyOpen != this) + await _currentlyOpen.StartCollapseAsync(); - _currentlyOpen = this; - CollapsiblePanel.Visibility = Visibility.Visible; + _currentlyOpen = this; + CollapsiblePanel.Visibility = Visibility.Visible; - VisualStateManager.GoToState(this, "ExpandedState", true); - await ExpandAsync(); // wait for your own expand - } - else - { - if (_currentlyOpen == this) _currentlyOpen = null; - await StartCollapseAsync(); - } + VisualStateManager.GoToState(this, "ExpandedState", true); + await ExpandAsync(); } - finally + else { - _sem.Release(); + if (_currentlyOpen == this) _currentlyOpen = null; + await StartCollapseAsync(); } } From 00894be2564c4df2147a2f2dc15b830d10d4f9ba Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 15:05:36 +0200 Subject: [PATCH 3/9] cleanup of changes --- App/Controls/ExpandContent.xaml | 21 +++++++++++---------- App/ViewModels/TrayWindowViewModel.cs | 2 -- App/Views/TrayWindow.xaml.cs | 5 ----- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index 788264a..aa47fc5 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -1,18 +1,19 @@ + + + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:toolkit="using:CommunityToolkit.WinUI" + mc:Ignorable="d"> - - + + - + @@ -27,7 +28,7 @@ - @@ -42,7 +43,7 @@ - diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 71ee9cc..24b1b99 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -362,8 +362,6 @@ private void ShowFileSyncListWindow() [RelayCommand] private async Task SignOut() { - //if (VpnLifecycle is not VpnLifecycle.Stopped) - // return; await _rpcController.StopVpn(); await _credentialManager.ClearCredentials(); } diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 7bc7b3c..4745e52 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -34,7 +34,6 @@ public sealed partial class TrayWindow : Window private bool _resizeInProgress; private NativeApi.POINT? _lastActivatePosition; - private int _maxHeightSinceLastActivation; private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; @@ -209,7 +208,6 @@ private void MoveAndResize(double height) private void MoveResizeAndActivate() { SaveCursorPos(); - _maxHeightSinceLastActivation = 0; MoveAndResize(RootFrame.GetContentSize().Height); AppWindow.Show(); NativeApi.SetForegroundWindow(WindowNative.GetWindowHandle(this)); @@ -234,9 +232,6 @@ private SizeInt32 CalculateWindowSize(double height) var scale = DisplayScale.WindowScale(this); var newWidth = (int)(WIDTH * scale); var newHeight = (int)(height * scale); - // Store the maximum height we've seen for positioning purposes. - if (newHeight > _maxHeightSinceLastActivation) - _maxHeightSinceLastActivation = newHeight; return new SizeInt32(newWidth, newHeight); } From 3d8621c5f2572c3368d20d48e1531b701214bd75 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 15:09:13 +0200 Subject: [PATCH 4/9] fixes --- App/Controls/ExpandContent.xaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index aa47fc5..fa482b8 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -9,13 +9,11 @@ xmlns:toolkit="using:CommunityToolkit.WinUI" mc:Ignorable="d"> - - + - - - - + + + From 557c5d1d67f17541efd4958704f5936884e080b4 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 15:10:06 +0200 Subject: [PATCH 5/9] format --- App/Views/TrayWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 4745e52..124b756 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -109,7 +109,7 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan var size = AppWindow.Size; // Shift upward when height increases - pos.Y -= delta; + pos.Y -= delta; size.Height = newHeight; AppWindow.MoveAndResize(new RectInt32(pos.X, pos.Y, size.Width, size.Height)); From 18d785f651f55e41b513c5324d8e1cbf7ad47e17 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 27 May 2025 15:10:39 +0200 Subject: [PATCH 6/9] format --- App/Controls/ExpandContent.xaml | 1 - 1 file changed, 1 deletion(-) diff --git a/App/Controls/ExpandContent.xaml b/App/Controls/ExpandContent.xaml index fa482b8..2cc0eb4 100644 --- a/App/Controls/ExpandContent.xaml +++ b/App/Controls/ExpandContent.xaml @@ -10,7 +10,6 @@ mc:Ignorable="d"> - From 1496bece96659c3891667528f6eb66d12e4c1d39 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 May 2025 11:10:18 +0200 Subject: [PATCH 7/9] Update App/Services/RpcController.cs Co-authored-by: Dean Sheather --- App/Services/RpcController.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index ca5ef7b..7beff66 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -235,10 +235,7 @@ public async Task StopVpn(CancellationToken ct = default) throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}"); } - if (reply.Stop.Success) - { - MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); - } + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); } public async ValueTask DisposeAsync() From 2efcdd4ece104762dc7707b0eba50843b780b3a7 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 May 2025 12:46:27 +0200 Subject: [PATCH 8/9] WIP --- App/Controls/ExpandContent.xaml.cs | 18 +++++++--- App/Views/TrayWindow.xaml | 7 ++-- App/Views/TrayWindow.xaml.cs | 55 +++++++++++++++++++----------- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/App/Controls/ExpandContent.xaml.cs b/App/Controls/ExpandContent.xaml.cs index 2e9bcb3..89b9ac5 100644 --- a/App/Controls/ExpandContent.xaml.cs +++ b/App/Controls/ExpandContent.xaml.cs @@ -3,7 +3,6 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Markup; using System; -using System.Threading; using System.Threading.Tasks; namespace Coder.Desktop.App.Controls; @@ -23,17 +22,26 @@ public ExpandContent() InitializeComponent(); Loaded += (_, __) => { - if (_pendingIsOpen is bool v) + // When we load the control for the first time (after panel swapping) + // we need to set the initial state based on IsOpen. + VisualStateManager.GoToState( + this, + IsOpen ? "ExpandedState" : "CollapsedState", + useTransitions: false); // ← NO animation yet + + // If IsOpen was already true we must also show the panel + if (IsOpen) { - _ = AnimateAsync(v); - _pendingIsOpen = null; + CollapsiblePanel.Visibility = Visibility.Visible; + // This makes the panel expand to its full height + CollapsiblePanel.ClearValue(FrameworkElement.MaxHeightProperty); } }; } partial void OnIsOpenChanged(bool oldValue, bool newValue) { - if (!this.IsLoaded) + if (!IsLoaded) { _pendingIsOpen = newValue; return; diff --git a/App/Views/TrayWindow.xaml b/App/Views/TrayWindow.xaml index 7ceba9d..cfc4214 100644 --- a/App/Views/TrayWindow.xaml +++ b/App/Views/TrayWindow.xaml @@ -23,8 +23,9 @@ + Width="0" + Height="0" + IsHitTestVisible="False" + Opacity="0" /> diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 124b756..13aa273 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -12,6 +12,7 @@ using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Animation; using System; +using System.Diagnostics; using System.Runtime.InteropServices; using Windows.Graphics; using Windows.System; @@ -31,7 +32,7 @@ public sealed partial class TrayWindow : Window // This is used to know the "start point of the animation" private int _lastWindowHeight; - private bool _resizeInProgress; + private Storyboard? _currentSb; private NativeApi.POINT? _lastActivatePosition; @@ -93,26 +94,26 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan _aw = AppWindow.GetFromWindowId( Win32Interop.GetWindowIdFromWindow( - WinRT.Interop.WindowNative.GetWindowHandle(this))); - SizeProxy.Height = 0; + WindowNative.GetWindowHandle(this))); SizeProxy.SizeChanged += (_, e) => { - if (!_resizeInProgress) return; + if (_currentSb is null) return; // nothing running - var newHeight = (int)Math.Round( + int newHeight = (int)Math.Round( e.NewSize.Height * DisplayScale.WindowScale(this)); - var delta = newHeight - _lastWindowHeight; + int delta = newHeight - _lastWindowHeight; if (delta == 0) return; - var pos = AppWindow.Position; - var size = AppWindow.Size; + var pos = _aw.Position; + var size = _aw.Size; - // Shift upward when height increases - pos.Y -= delta; + pos.Y -= delta; // grow upward size.Height = newHeight; - AppWindow.MoveAndResize(new RectInt32(pos.X, pos.Y, size.Width, size.Height)); + _aw.MoveAndResize( + new RectInt32(pos.X, pos.Y, size.Width, size.Height)); + _lastWindowHeight = newHeight; }; } @@ -179,9 +180,16 @@ private void RootFrame_SizeChanged(object sender, SizedFrameEventArgs e) private void AnimateWindowHeight(double targetHeight) { - // Remember where we start + // If another animation is already running we need to fast forward it. + if (_currentSb is { } oldSb) + { + oldSb.Completed -= OnStoryboardCompleted; + // We need to use SkipToFill, because Stop actually sets Height to 0, which + // makes the window go haywire. + oldSb.SkipToFill(); + } + _lastWindowHeight = AppWindow.Size.Height; - _resizeInProgress = true; var anim = new DoubleAnimation { @@ -194,21 +202,28 @@ private void AnimateWindowHeight(double targetHeight) Storyboard.SetTarget(anim, SizeProxy); Storyboard.SetTargetProperty(anim, "Height"); - new Storyboard { Children = { anim } }.Begin(); + var sb = new Storyboard { Children = { anim } }; + sb.Completed += OnStoryboardCompleted; + sb.Begin(); + + _currentSb = sb; } - private void MoveAndResize(double height) + private void OnStoryboardCompleted(object? sender, object e) { - var size = CalculateWindowSize(height); - var pos = CalculateWindowPosition(size); - var rect = new RectInt32(pos.X, pos.Y, size.Width, size.Height); - AppWindow.MoveAndResize(rect); + if (sender is Storyboard sb) + sb.Completed -= OnStoryboardCompleted; + + _currentSb = null; } private void MoveResizeAndActivate() { SaveCursorPos(); - MoveAndResize(RootFrame.GetContentSize().Height); + var size = CalculateWindowSize(RootFrame.GetContentSize().Height); + var pos = CalculateWindowPosition(size); + var rect = new RectInt32(pos.X, pos.Y, size.Width, size.Height); + AppWindow.MoveAndResize(rect); AppWindow.Show(); NativeApi.SetForegroundWindow(WindowNative.GetWindowHandle(this)); } From ef69579b1a814b344e7c4ac4db7e004f648b7e92 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 May 2025 13:08:35 +0200 Subject: [PATCH 9/9] added comments, simplified logic --- App/Controls/ExpandContent.xaml.cs | 60 ++++++------------------------ App/Views/TrayWindow.xaml.cs | 7 ++++ 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/App/Controls/ExpandContent.xaml.cs b/App/Controls/ExpandContent.xaml.cs index 89b9ac5..926af9a 100644 --- a/App/Controls/ExpandContent.xaml.cs +++ b/App/Controls/ExpandContent.xaml.cs @@ -14,8 +14,8 @@ public sealed partial class ExpandContent : UserControl { public UIElementCollection Children => CollapsiblePanel.Children; - private bool? _pendingIsOpen; - + private readonly string _expandedState = "ExpandedState"; + private readonly string _collapsedState = "CollapsedState"; public ExpandContent() { @@ -26,8 +26,8 @@ public ExpandContent() // we need to set the initial state based on IsOpen. VisualStateManager.GoToState( this, - IsOpen ? "ExpandedState" : "CollapsedState", - useTransitions: false); // ← NO animation yet + IsOpen ? _expandedState : _collapsedState, + useTransitions: false); // NO animation yet // If IsOpen was already true we must also show the panel if (IsOpen) @@ -41,59 +41,21 @@ public ExpandContent() partial void OnIsOpenChanged(bool oldValue, bool newValue) { - if (!IsLoaded) + var newState = newValue ? _expandedState : _collapsedState; + if (newValue) { - _pendingIsOpen = newValue; - return; - } - _ = AnimateAsync(newValue); - } - - private async Task AnimateAsync(bool open) - { - if (open) - { - if (_currentlyOpen is not null && _currentlyOpen != this) - await _currentlyOpen.StartCollapseAsync(); - - _currentlyOpen = this; CollapsiblePanel.Visibility = Visibility.Visible; - - VisualStateManager.GoToState(this, "ExpandedState", true); - await ExpandAsync(); + // We use BeginTime to ensure other panels are collapsed first. + // If the user clicks the expand button quickly, we want to avoid + // the panel expanding to its full height before the collapse animation completes. + CollapseSb.SkipToFill(); } - else - { - if (_currentlyOpen == this) _currentlyOpen = null; - await StartCollapseAsync(); - } - } - - private static ExpandContent? _currentlyOpen; - private TaskCompletionSource? _collapseTcs; - - private async Task ExpandAsync() - { - CollapsiblePanel.Visibility = Visibility.Visible; - VisualStateManager.GoToState(this, "ExpandedState", true); - - var tcs = new TaskCompletionSource(); - void done(object? s, object e) { ExpandSb.Completed -= done; tcs.SetResult(); } - ExpandSb.Completed += done; - await tcs.Task; - } - private Task StartCollapseAsync() - { - _collapseTcs = new TaskCompletionSource(); - VisualStateManager.GoToState(this, "CollapsedState", true); - return _collapseTcs.Task; + VisualStateManager.GoToState(this, newState, true); } private void CollapseStoryboard_Completed(object sender, object e) { CollapsiblePanel.Visibility = Visibility.Collapsed; - _collapseTcs?.TrySetResult(); - _collapseTcs = null; } } diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 13aa273..ef55095 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -178,6 +178,9 @@ private void RootFrame_SizeChanged(object sender, SizedFrameEventArgs e) AnimateWindowHeight(e.NewSize.Height); } + // We need to animate the height change in code-behind, because XAML + // storyboard animation timeline is immutable - it cannot be changed + // mid-run to accomodate a new height. private void AnimateWindowHeight(double targetHeight) { // If another animation is already running we need to fast forward it. @@ -211,9 +214,13 @@ private void AnimateWindowHeight(double targetHeight) private void OnStoryboardCompleted(object? sender, object e) { + // We need to remove the event handler after the storyboard completes, + // to avoid memory leaks and multiple calls. if (sender is Storyboard sb) sb.Completed -= OnStoryboardCompleted; + // SizeChanged handler will stop forwarding resize ticks + // until we start the next storyboard. _currentSb = null; }