Skip to content

Commit

Permalink
Freeze layouts calculations on resize (Closes #734) (#735)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexMacocian authored May 22, 2024
1 parent 9721a7e commit ea33ee6
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 2 deletions.
28 changes: 28 additions & 0 deletions Daybreak/Behaviors/LayoutFreezeDecorator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Windows;
using System.Windows.Controls;

namespace Daybreak.Behaviors;
internal sealed class LayoutFreezeDecorator : Decorator
{
public bool IsLayoutFrozen { get; set; }

protected override Size MeasureOverride(Size constraint)
{
if (this.IsLayoutFrozen)
{
return new Size(0, 0);
}

return base.MeasureOverride(constraint);
}

protected override Size ArrangeOverride(Size arrangeSize)
{
if (this.IsLayoutFrozen)
{
return arrangeSize;
}

return base.ArrangeOverride(arrangeSize);
}
}
3 changes: 3 additions & 0 deletions Daybreak/Configuration/ProjectConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
using Daybreak.Services.Browser;
using Daybreak.Services.ApplicationArguments;
using Daybreak.Services.ApplicationArguments.ArgumentHandling;
using Daybreak.Services.Window;
using Daybreak.Launch;

namespace Daybreak.Configuration;

Expand Down Expand Up @@ -228,6 +230,7 @@ public override void RegisterServices(IServiceCollection services)
services.AddSingleton<ISevenZipExtractor, SevenZipExtractor>();
services.AddSingleton<IGraphClient, GraphClient>();
services.AddSingleton<IOptionsSynchronizationService, OptionsSynchronizationService>();
services.AddSingleton<IWindowEventsHook<MainWindow>, WindowEventsHook<MainWindow>>();
services.AddScoped<IBrowserExtensionsManager, BrowserExtensionsManager>();
services.AddScoped<IBrowserExtensionsProducer, BrowserExtensionsManager>(sp => sp.GetRequiredService<IBrowserExtensionsManager>().Cast<BrowserExtensionsManager>());
services.AddScoped<ICredentialManager, CredentialManager>();
Expand Down
4 changes: 4 additions & 0 deletions Daybreak/Launch/Launcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Daybreak.Services.Startup;
using Daybreak.Services.Themes;
using Daybreak.Services.Updater.PostUpdate;
using Daybreak.Services.Window;
using Daybreak.Views;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -150,6 +151,9 @@ protected override void ApplicationStarting()
this.logger.LogError(e, "Encountered exception while loading plugins. Aborting...");
this.exceptionHandler.HandleException(e);
}

// Trigger hooks into MainWindow
this.ServiceProvider.GetRequiredService<IWindowEventsHook<MainWindow>>();

startupStatus.CurrentStep = StartupStatus.Custom("Registering view container");
this.RegisterViewContainer();
Expand Down
5 changes: 5 additions & 0 deletions Daybreak/Launch/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ private async void UpdateRandomImage()
return;
}

if (!this.IsEnabled)
{
return;
}

var response = await this.backgroundProvider.GetBackground();
if (response.ImageSource is null)
{
Expand Down
12 changes: 12 additions & 0 deletions Daybreak/Services/Window/IWindowEventsHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Daybreak.Services.Window;

public interface IWindowEventsHook<T> : IDisposable
where T : System.Windows.Window
{
void RegisterHookOnSizeOrMoveBegin(Action hook);
void RegisterHookOnSizeOrMoveEnd(Action hook);
void UnregisterHookOnSizeOrMoveBegin(Action hook);
void UnregisterHookOnSizeOrMoveEnd(Action hook);
}
126 changes: 126 additions & 0 deletions Daybreak/Services/Window/WindowEventsHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Daybreak.Behaviors;
using System;
using System.Collections.Generic;
using System.Core.Extensions;
using System.Extensions;
using System.Windows;
using System.Windows.Interop;

namespace Daybreak.Services.Window;

internal sealed class WindowEventsHook<T> : IWindowEventsHook<T>
where T : System.Windows.Window
{
private const int WM_ENTERSIZEMOVE = 0x0231;
private const int WM_EXITSIZEMOVE = 0x0232;

private readonly List<Action> sizeMoveBeginHooks = [];
private readonly List<Action> sizeMoveEndHooks = [];
private readonly T window;

private HwndSource? hwndSource;

public WindowEventsHook(
T window)
{
this.window = window.ThrowIfNull();

// If the window is loaded, hook directly. Otherwise, wait for the window to finish loading before hooking
if (this.window.IsLoaded)
{
this.HookIntoWindow();
}
else
{
this.window.Loaded += this.MainWindow_Loaded;
}
}

public void RegisterHookOnSizeOrMoveBegin(Action hook)
{
this.sizeMoveBeginHooks.Add(hook);
}

public void RegisterHookOnSizeOrMoveEnd(Action hook)
{
this.sizeMoveEndHooks.Add(hook);
}

public void UnregisterHookOnSizeOrMoveBegin(Action hook)
{
this.sizeMoveBeginHooks.Remove(hook);
}

public void UnregisterHookOnSizeOrMoveEnd(Action hook)
{
this.sizeMoveEndHooks.Remove(hook);
}

private void MainWindow_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
this.HookIntoWindow();
}

private void HookIntoWindow()
{
this.hwndSource = PresentationSource.FromVisual(this.window).Cast<HwndSource>();
this.hwndSource.AddHook(this.WndProc);
var contentCache = this.window.Content.Cast<UIElement>();
var decorator = new LayoutFreezeDecorator();
this.window.Content = decorator;
decorator.Child = contentCache;
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_ENTERSIZEMOVE:
// Disable layout changes while resizing
if (this.window.Content is LayoutFreezeDecorator freezeDecorator)
{
freezeDecorator.IsLayoutFrozen = true;
}

this.window.Dispatcher.Invoke(() =>
{
foreach (var action in this.sizeMoveBeginHooks)
{
action();
}
});

break;
case WM_EXITSIZEMOVE:
// Re-enable layout changes after resizing is done
if (this.window.Content is LayoutFreezeDecorator freezeDecorator2)
{
freezeDecorator2.IsLayoutFrozen = false;
// Force layout recalculation to fit the new content to the new size
freezeDecorator2.InvalidateArrange();
freezeDecorator2.InvalidateMeasure();
freezeDecorator2.InvalidateVisual();
}

this.window.Dispatcher.Invoke(() =>
{
foreach (var action in this.sizeMoveEndHooks)
{
action();
}
});

break;
}

return IntPtr.Zero;
}

public void Dispose()
{
this.hwndSource?.Dispose();
this.sizeMoveBeginHooks.Clear();
this.sizeMoveEndHooks.Clear();
this.window.Loaded -= this.MainWindow_Loaded;
}
}
75 changes: 75 additions & 0 deletions Daybreak/Views/FocusView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Daybreak.Services.Notifications;
using Daybreak.Services.Scanner;
using Daybreak.Services.Screens;
using Daybreak.Services.Window;
using Daybreak.Views.Trade;
using Microsoft.Extensions.Logging;
using System;
Expand Down Expand Up @@ -47,6 +48,7 @@ public partial class FocusView : UserControl
private static readonly TimeSpan InventoryDataFrequency = TimeSpan.FromSeconds(1);
private static readonly TimeSpan CartoDataFrequency = TimeSpan.FromSeconds(1);

private readonly IWindowEventsHook<MainWindow> mainWindowEventsHook;
private readonly INotificationService notificationService;
private readonly IBuildTemplateManager buildTemplateManager;
private readonly IApplicationLauncher applicationLauncher;
Expand Down Expand Up @@ -104,13 +106,18 @@ public partial class FocusView : UserControl
[GenerateDependencyProperty]
private CartoProgressContext cartoTitle = default!;

[GenerateDependencyProperty]
private bool pauseDataFetching;

private IWindowEventsHook<MinimapWindow>? minimapWindowEventsHook;
private MinimapWindow? minimapWindow;
private bool browserMaximized = false;
private bool minimapMaximized = false;
private bool inventoryMaximized = false;
private CancellationTokenSource? cancellationTokenSource;

public FocusView(
IWindowEventsHook<MainWindow> mainWindowEventsHook,
INotificationService notificationService,
IBuildTemplateManager buildTemplateManager,
IApplicationLauncher applicationLauncher,
Expand All @@ -123,6 +130,7 @@ public FocusView(
ILiveUpdateableOptions<MinimapWindowOptions> minimapWindowOptions,
ILogger<FocusView> logger)
{
this.mainWindowEventsHook = mainWindowEventsHook.ThrowIfNull();
this.notificationService = notificationService.ThrowIfNull();
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
this.applicationLauncher = applicationLauncher.ThrowIfNull();
Expand Down Expand Up @@ -208,6 +216,12 @@ private async void PeriodicallyReadPathingData(CancellationToken cancellationTok
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(PathingDataFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(PathingDataFrequency, cancellationToken);
Expand Down Expand Up @@ -282,6 +296,12 @@ private async void PeriodicallyReadGameState(CancellationToken cancellationToken
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(GameStateFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(GameStateFrequency, cancellationToken);
Expand Down Expand Up @@ -361,6 +381,12 @@ private async void PeriodicallyReadCartoData(CancellationToken cancellationToken
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(CartoDataFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(CartoDataFrequency, cancellationToken);
Expand Down Expand Up @@ -486,6 +512,12 @@ private async void PeriodicallyReadGameData(CancellationToken cancellationToken)
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(GameDataFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(GameDataFrequency, cancellationToken);
Expand Down Expand Up @@ -590,6 +622,12 @@ private async void PeriodicallyReadInventoryData(CancellationToken cancellationT
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(InventoryDataFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(InventoryDataFrequency, cancellationToken);
Expand Down Expand Up @@ -666,6 +704,12 @@ private async void PeriodicallyReadMainPlayerContextData(CancellationToken cance
return;
}

if (this.PauseDataFetching)
{
await Task.Delay(MainPlayerDataFrequency, cancellationToken);
continue;
}

if (this.DataContext is not GuildWarsApplicationLaunchContext context)
{
await Task.Delay(MainPlayerDataFrequency, cancellationToken);
Expand Down Expand Up @@ -762,6 +806,8 @@ private async void FocusView_Loaded(object _, RoutedEventArgs e)
return;
}

this.mainWindowEventsHook.RegisterHookOnSizeOrMoveBegin(this.OnMainWindowSizeOrMoveStart);
this.mainWindowEventsHook.RegisterHookOnSizeOrMoveEnd(this.OnMainWindowSizeOrMoveEnd);
this.Browser.BrowserHistoryManager.SetBrowserHistory(this.liveUpdateableOptions.Value.BrowserHistory);
this.InventoryVisible = this.liveUpdateableOptions.Value.InventoryComponentVisible;
this.MinimapVisible = this.liveUpdateableOptions.Value.MinimapComponentVisible;
Expand All @@ -786,6 +832,8 @@ private async void FocusView_Loaded(object _, RoutedEventArgs e)

private void FocusView_Unloaded(object _, RoutedEventArgs e)
{
this.mainWindowEventsHook.UnregisterHookOnSizeOrMoveBegin(this.OnMainWindowSizeOrMoveStart);
this.mainWindowEventsHook.UnregisterHookOnSizeOrMoveEnd(this.OnMainWindowSizeOrMoveEnd);
this.cancellationTokenSource?.Cancel();
this.cancellationTokenSource = null;
this.GameData = default;
Expand Down Expand Up @@ -993,6 +1041,9 @@ private void MinimapExtractButton_Clicked(object sender, EventArgs e)
this.MinimapHolder.Children.Remove(this.MinimapComponent);
this.minimapWindow.Content = this.MinimapComponent;
this.minimapWindow.Show();
this.minimapWindowEventsHook = new WindowEventsHook<MinimapWindow>(this.minimapWindow);
this.minimapWindowEventsHook.RegisterHookOnSizeOrMoveBegin(this.OnMinimapWindowSizeOrMoveStart);
this.minimapWindowEventsHook.RegisterHookOnSizeOrMoveEnd(this.OnMinimapWindowSizeOrMoveEnd);
this.RowAutoMargin.RecalculateRows();
}

Expand Down Expand Up @@ -1026,7 +1077,31 @@ private void MinimapWindow_Closed(object? sender, EventArgs e)
this.minimapWindow.Closed -= this.MinimapWindow_Closed;
this.minimapWindow.Content = default!;
this.minimapWindow = default;
this.minimapWindowEventsHook?.Dispose();
this.minimapWindowEventsHook = default;
this.MinimapHolder.Children.Insert(0, this.MinimapComponent);
this.RowAutoMargin.RecalculateRows();
}

private void OnMainWindowSizeOrMoveStart()
{
this.IsEnabled = false;
this.PauseDataFetching = true;
}

private void OnMainWindowSizeOrMoveEnd()
{
this.IsEnabled = true;
this.PauseDataFetching = false;
}

private void OnMinimapWindowSizeOrMoveStart()
{
this.PauseDataFetching = true;
}

private void OnMinimapWindowSizeOrMoveEnd()
{
this.PauseDataFetching = false;
}
}
Loading

0 comments on commit ea33ee6

Please sign in to comment.