Remember MainWindow Position
ADD
WalletWasabi.Fluent/Behaviors/MainWindowPersistPositionBehavior.cs
+using Avalonia;
+using Avalonia.Controls;
+using ReactiveUI;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using WalletWasabi.Fluent.ViewModels;
+using System.Linq;
+
+namespace WalletWasabi.Fluent.Behaviors;
+
+public class MainWindowBindPositionBehavior : DisposingBehavior<Window>
+{
+ protected override void OnAttached(CompositeDisposable disposables)
+ {
+ if (AssociatedObject is null)
+ {
+ return;
+ }
+
+ Observable.FromEventPattern(AssociatedObject, nameof(Window.DataContextChanged))
+ .Subscribe(_ => BindPosition(disposables))
+ .DisposeWith(disposables);
+ }
+
+ private void BindPosition(CompositeDisposable disposables)
+ {
+ if (AssociatedObject!.DataContext is MainViewModel vm)
+ {
+ Observable.FromEventPattern(AssociatedObject, nameof(Window.PositionChanged))
+ .Subscribe(_ => vm.WindowPosition = AssociatedObject.Position)
+ .DisposeWith(disposables);
+
+ vm.WhenAnyValue(x => x.WindowPosition)
+ .Subscribe(x => AssociatedObject.Position = x)
+ .DisposeWith(disposables);
+
+ SetInitialPosition(vm);
+ }
+ }
+
+ private void SetInitialPosition(MainViewModel vm)
+ {
+ if (vm.WindowState != WindowState.Normal)
+ {
+ return;
+ }
+
+ var position = vm.WindowPosition;
+
+ var isValidLeft =
+ AssociatedObject!.Screens.All.Any(s => s.Bounds.X <= position.X);
+
+ var isValidRight =
+ AssociatedObject.Screens.All.Any(s => s.Bounds.Right >= position.X);
+
+ var isValidTop =
+ AssociatedObject!.Screens.All.Any(s => s.Bounds.TopLeft.Y <= position.Y);
+
+ var isValidBottom =
+ AssociatedObject.Screens.All.Any(s => s.Bounds.BottomRight.Y >= position.Y);
+
+ var x =
+ isValidLeft && isValidRight
+ ? position.X
+ : (int)((AssociatedObject.Screens.Primary.Bounds.Width - vm.WindowWidth) / 2);
+
+ var y =
+ isValidTop && isValidBottom
+ ? position.Y
+ : (int)((AssociatedObject.Screens.Primary.Bounds.Height - vm.WindowHeight) / 2);
+
+ AssociatedObject.Position = new PixelPoint(x, y);
+
+ var currentScreen = AssociatedObject.Screens.ScreenFromPoint(AssociatedObject.Position);
+
+ var isValidWidth =
+ vm.WindowWidth * currentScreen.PixelDensity <= currentScreen.WorkingArea.Width * currentScreen.PixelDensity;
+
+ var isValidHeight =
+ vm.WindowHeight * currentScreen.PixelDensity <= currentScreen.WorkingArea.Height;
+
+ if (!isValidWidth || !isValidHeight)
+ {
+ vm.WindowState = WindowState.Maximized;
+ }
+ }
+}

MODIFY
WalletWasabi.Fluent/UiConfig.cs
WalletWasabi.Fluent/UiConfig.cs
@@ -18,6 +18,10 @@ public class UiConfig : ConfigBase
private bool _darkModeEnabled;
private string? _lastSelectedWallet;
private string _windowState = "Normal";
+ private int? _windowX;
+ private int? _windowY;
+ private double? _windowWidth;
+ private double? _windowHeight;
private bool _runOnSystemStartup;
private bool _oobe;
private bool _hideOnClose;
@@ -48,6 +52,18 @@ public class UiConfig : ConfigBase
.Skip(1) // Won't save on UiConfig creation.
.ObserveOn(RxApp.TaskpoolScheduler)
.Subscribe(_ => ToFile());
+
+ this.WhenAnyValue(
+ x => x.WindowState,
+ x => x.WindowX,
+ x => x.WindowY,
+ x => x.WindowWidth,
+ x => x.WindowHeight,
+ (_, _, _, _, _) => Unit.Default)
+ .Throttle(TimeSpan.FromMilliseconds(500))
+ .Skip(1) // Won't save on UiConfig creation.
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(_ => ToFile());
}
[JsonProperty(PropertyName = "Oobe", DefaultValueHandling = DefaultValueHandling.Populate)]
@@ -66,6 +82,34 @@ public class UiConfig : ConfigBase
internal set => RaiseAndSetIfChanged(ref _windowState, value);
}
+ [JsonProperty(PropertyName = "WindowX")]
+ public int? WindowX
+ {
+ get => _windowX;
+ internal set => RaiseAndSetIfChanged(ref _windowX, value);
+ }
+
+ [JsonProperty(PropertyName = "WindowY")]
+ public int? WindowY
+ {
+ get => _windowY;
+ internal set => RaiseAndSetIfChanged(ref _windowY, value);
+ }
+
+ [JsonProperty(PropertyName = "WindowWidth")]
+ public double? WindowWidth
+ {
+ get => _windowWidth;
+ internal set => RaiseAndSetIfChanged(ref _windowWidth, value);
+ }
+
+ [JsonProperty(PropertyName = "WindowHeight")]
+ public double? WindowHeight
+ {
+ get => _windowHeight;
+ internal set => RaiseAndSetIfChanged(ref _windowHeight, value);
+ }
+
[DefaultValue(2)]
[JsonProperty(PropertyName = "FeeTarget", DefaultValueHandling = DefaultValueHandling.Populate)]
public int FeeTarget

MODIFY
WalletWasabi.Fluent/ViewModels/MainViewModel.cs
WalletWasabi.Fluent/ViewModels/MainViewModel.cs
@@ -18,6 +18,7 @@ using WalletWasabi.Logging;
using WalletWasabi.Fluent.ViewModels.StatusBar;
using WalletWasabi.Fluent.ViewModels.Wallets;
using WalletWasabi.WabiSabi.Client;
+using Avalonia;
namespace WalletWasabi.Fluent.ViewModels;
@@ -39,10 +40,22 @@ public partial class MainViewModel : ViewModelBase
[AutoNotify] private WindowState _windowState;
[AutoNotify] private bool _isOobeBackgroundVisible;
[AutoNotify] private bool _isCoinJoinActive;
+ [AutoNotify] private double _windowWidth;
+ [AutoNotify] private double _windowHeight;
+ [AutoNotify] private PixelPoint _windowPosition;
public MainViewModel()
{
_windowState = (WindowState)Enum.Parse(typeof(WindowState), Services.UiConfig.WindowState);
+ _windowWidth = Services.UiConfig.WindowWidth ?? 1280;
+ _windowHeight = Services.UiConfig.WindowHeight ?? 960;
+
+ var (x, y) = (Services.UiConfig.WindowX, Services.UiConfig.WindowY);
+ if (x != null && y != null)
+ {
+ _windowPosition = new PixelPoint(x.Value, y.Value);
+ }
+
_dialogScreen = new DialogScreenViewModel();
_fullScreen = new DialogScreenViewModel(NavigationTarget.FullScreen);
@@ -78,10 +91,17 @@ public partial class MainViewModel : ViewModelBase
_searchPage.Initialise();
- this.WhenAnyValue(x => x.WindowState)
- .Where(x => x != WindowState.Minimized)
+ this.WhenAnyValue(x => x.WindowState, x => x.WindowPosition, x => x.WindowWidth, x => x.WindowHeight)
+ .Where(x => x.Item1 != WindowState.Minimized)
.ObserveOn(RxApp.MainThreadScheduler)
- .Subscribe(windowState => Services.UiConfig.WindowState = windowState.ToString());
+ .Subscribe(t =>
+ {
+ Services.UiConfig.WindowState = t.Item1.ToString();
+ Services.UiConfig.WindowX = t.Item2.X;
+ Services.UiConfig.WindowY = t.Item2.Y;
+ Services.UiConfig.WindowWidth = t.Item3;
+ Services.UiConfig.WindowHeight = t.Item4;
+ });
this.WhenAnyValue(
x => x.DialogScreen!.IsDialogOpen,

MODIFY
WalletWasabi.Fluent/Views/MainWindow.axaml
WalletWasabi.Fluent/Views/MainWindow.axaml
x:Class="WalletWasabi.Fluent.Views.MainWindow"
x:Name="MainWindow"
MinWidth="640" MinHeight="480"
- WindowStartupLocation="CenterScreen"
+ Width="{Binding WindowWidth, Mode=TwoWay}"
+ Height="{Binding WindowHeight, Mode=TwoWay}"
+ WindowStartupLocation="Manual"
Background="{x:Null}"
TransparencyLevelHint="Mica"
ExtendClientAreaToDecorationsHint="True"
<behaviors:RegisterNotificationHostBehavior />
<behaviors:HideShowBehavior />
<behaviors:ShowWalletCoinsOnKeyCombinationBehavior Key1="LeftCtrl" Key2="D" Key3="C" />
+ <behaviors:MainWindowBindPositionBehavior/>
</i:Interaction.Behaviors>
<Panel Margin="{Binding #MainWindow.OffScreenMargin}">

Remember MainWindow Position