mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 10:48:23 +00:00
Added a setting to change the size of the interface (#1136)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
c2b62c6dbd
commit
e0337a99bc
27 files changed
+1742
-27
No files matched your search
@@ -0,0 +1,39 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// What the runtime reports about the interface size on the screen AI Studio shows on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All percentages count like the scaling of the operating system: device pixels per pixel of the app.
|
||||
/// </remarks>
|
||||
/// <param name="Success">Whether the runtime could measure the screen.</param>
|
||||
/// <param name="ErrorMessage">What went wrong, when it could not.</param>
|
||||
/// <param name="Zoom">The zoom of the webview: 1.0 is the scaling of the system.</param>
|
||||
/// <param name="Size">The size on this screen: the one the user chose for it, or else its default size.</param>
|
||||
/// <param name="DefaultSize">The size of this screen while the user chose none for it: the app default on the screen of a laptop, the scaling of the system on any other.</param>
|
||||
/// <param name="ScreenKey">What the size of this screen is stored by: its resolution and its physical size.</param>
|
||||
/// <param name="ScreenWidth">The width of the screen in device pixels.</param>
|
||||
/// <param name="ScreenHeight">The height of the screen in device pixels.</param>
|
||||
/// <param name="ScreenDiagonalInches">The diagonal of the screen in inches, or null when the screen does not report its size.</param>
|
||||
/// <param name="SystemPercent">The scaling of the system on this screen.</param>
|
||||
/// <param name="CurrentPercent">The scale AI Studio shows with.</param>
|
||||
/// <param name="AppDefaultPercent">The scale the app default gives this screen, which is also where its custom scale starts.</param>
|
||||
/// <param name="MinPercent">The smallest custom scale this screen allows.</param>
|
||||
/// <param name="MaxPercent">The largest custom scale this screen allows.</param>
|
||||
public sealed record InterfaceSizeResponse(
|
||||
bool Success,
|
||||
string ErrorMessage,
|
||||
double Zoom,
|
||||
InterfaceSize Size,
|
||||
InterfaceSize DefaultSize,
|
||||
string ScreenKey,
|
||||
int ScreenWidth,
|
||||
int ScreenHeight,
|
||||
double? ScreenDiagonalInches,
|
||||
int SystemPercent,
|
||||
int CurrentPercent,
|
||||
int AppDefaultPercent,
|
||||
int MinPercent,
|
||||
int MaxPercent);
|
||||
@@ -0,0 +1,9 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Asks the runtime to show AI Studio in the sizes the user chose.
|
||||
/// </summary>
|
||||
/// <param name="Screens">What the user chose for the screens they set a size for, each screen once. Any other screen gets its default size.</param>
|
||||
public sealed record SetInterfaceSizeRequest(List<InterfaceScreenSize> Screens);
|
||||
@@ -19,4 +19,6 @@ public enum TauriEventType
|
||||
|
||||
GLOBAL_SHORTCUT_PRESSED,
|
||||
GLOBAL_SHORTCUT_CHANGED,
|
||||
|
||||
INTERFACE_SIZE_CHANGED,
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Hands the interface sizes the user chose to the runtime, which zooms the webview accordingly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The runtime shows every screen in its default size from the moment the window opens, keeps the
|
||||
/// sizes, measures the screen again whenever the window moves to another one, and applies the size of
|
||||
/// that screen. This service only tells it what the user chose: as soon as the settings have loaded,
|
||||
/// and again whenever they change.
|
||||
/// </remarks>
|
||||
public sealed class InterfaceSizeService : BackgroundService, IMessageBusReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The smallest custom scale on any screen, in percent. A screen may allow less of the range,
|
||||
/// which the runtime reports. Keep in sync with MIN_SCALE in runtime/src/interface_zoom.rs.
|
||||
/// </summary>
|
||||
private const int MIN_CUSTOM_PERCENT = 60;
|
||||
|
||||
/// <summary>
|
||||
/// The largest custom scale on any screen, in percent. A screen may allow less of the range,
|
||||
/// which the runtime reports. Keep in sync with MAX_SCALE in runtime/src/interface_zoom.rs.
|
||||
/// </summary>
|
||||
private const int MAX_CUSTOM_PERCENT = 300;
|
||||
|
||||
/// <summary>
|
||||
/// How often the service looks whether the settings have loaded. Short, because a screen with a
|
||||
/// size of its own shows in its default size until then.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan SETTINGS_LOAD_CHECK_INTERVAL = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
private readonly SemaphoreSlim sendingSemaphore = new(1, 1);
|
||||
private readonly ILogger<InterfaceSizeService> logger;
|
||||
private readonly SettingsManager settingsManager;
|
||||
private readonly MessageBus messageBus;
|
||||
private readonly RustService rustService;
|
||||
private string? lastSentSignature;
|
||||
private volatile bool areSettingsLoaded;
|
||||
|
||||
public InterfaceSizeService(ILogger<InterfaceSizeService> logger, SettingsManager settingsManager, MessageBus messageBus, RustService rustService)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.settingsManager = settingsManager;
|
||||
this.messageBus = messageBus;
|
||||
this.rustService = rustService;
|
||||
|
||||
this.messageBus.RegisterComponent(this);
|
||||
this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
this.logger.LogInformation("The interface size service was initialized.");
|
||||
|
||||
//
|
||||
// The app loads the settings early, long before its startup completed with the plugins. The
|
||||
// sizes go to the runtime right then, so that a screen with a size of its own changes from
|
||||
// its default size as soon as possible:
|
||||
//
|
||||
while (!this.settingsManager.HasCompletedInitialSettingsLoad && !stoppingToken.IsCancellationRequested)
|
||||
await Task.Delay(SETTINGS_LOAD_CHECK_INTERVAL, stoppingToken);
|
||||
|
||||
this.areSettingsLoaded = true;
|
||||
await this.SendInterfaceSize();
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken);
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.messageBus.Unregister(this);
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#region IMessageBusReceiver
|
||||
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
// Until the settings have loaded, ExecuteAsync waits to send them:
|
||||
if (triggeredEvent is Event.CONFIGURATION_CHANGED && this.areSettingsLoaded)
|
||||
await this.SendInterfaceSize();
|
||||
}
|
||||
|
||||
public Task<MessageBusAnswer<TResult>> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult(MessageBusAnswer<TResult>.NONE);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Sends the chosen sizes to the runtime, unless the runtime has them already.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every change of any setting announces itself the same way, so most of them leave the sizes
|
||||
/// as they were. A scale beyond the range of any screen, e.g., in settings edited by hand, is
|
||||
/// brought within that range here, and the runtime narrows it to the screen.
|
||||
/// </remarks>
|
||||
private async Task SendInterfaceSize()
|
||||
{
|
||||
var screens = this.settingsManager.ConfigurationData.App.InterfaceScreenSizes
|
||||
.DistinctBy(screen => screen.ScreenKey)
|
||||
.Select(screen => screen with { CustomPercent = screen.CustomPercent is { } percent ? Math.Clamp(percent, MIN_CUSTOM_PERCENT, MAX_CUSTOM_PERCENT) : null })
|
||||
.ToList();
|
||||
|
||||
// The order of the screens tells when they were set, which the runtime does not need to know:
|
||||
var signature = string.Join(";", screens.OrderBy(screen => screen.ScreenKey, StringComparer.Ordinal).Select(screen => $"{screen.ScreenKey}={screen.Size}:{screen.CustomPercent}"));
|
||||
|
||||
await this.sendingSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (signature == this.lastSentSignature)
|
||||
return;
|
||||
|
||||
if (await this.rustService.SetInterfaceSize(new SetInterfaceSizeRequest(screens)) is not null)
|
||||
this.lastSentSignature = signature;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.sendingSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,4 +193,63 @@ public sealed partial class RustService
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the runtime to show AI Studio in the given size, which it reaches by zooming the webview.
|
||||
/// </summary>
|
||||
/// <param name="request">The size, and the scale of the custom size.</param>
|
||||
/// <returns>What the size comes to on the screen AI Studio shows on, or null when the runtime could not apply it.</returns>
|
||||
public async Task<InterfaceSizeResponse?> SetInterfaceSize(SetInterfaceSizeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await this.http.PostAsJsonAsync("/app/interface-size", request, this.jsonRustSerializerOptions);
|
||||
var result = await this.ReadInterfaceSizeResponse(response, "set the interface size");
|
||||
if (result is not null)
|
||||
this.logger?.LogInformation("Set the interface sizes of {ScreenCount} screens. AI Studio shows with the size '{Size}' at {CurrentPercent} % and a zoom of {Zoom:0.###} on the screen '{ScreenKey}'.", request.Screens.Count, result.Size, result.CurrentPercent, result.Zoom, result.ScreenKey);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger?.LogError(ex, "Exception while setting the interface size.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the runtime what the interface size comes to on the screen AI Studio shows on, e.g., which custom scales it allows.
|
||||
/// </summary>
|
||||
/// <returns>The report of the runtime, or null when it could not measure the screen.</returns>
|
||||
public async Task<InterfaceSizeResponse?> GetInterfaceSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await this.http.GetAsync("/app/interface-size");
|
||||
return await this.ReadInterfaceSizeResponse(response, "read the interface size");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger?.LogError(ex, "Exception while reading the interface size.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<InterfaceSizeResponse?> ReadInterfaceSizeResponse(HttpResponseMessage response, string action)
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger?.LogError("Failed to {Action} due to network error: {StatusCode}.", action, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<InterfaceSizeResponse>(this.jsonRustSerializerOptions);
|
||||
if (result is null || !result.Success)
|
||||
{
|
||||
this.logger?.LogError("Failed to {Action}: {Error}", action, result?.ErrorMessage ?? "Unknown error");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user