diff --git a/src/10-Core/Wtq/Configuration/OffScreenLocation.cs b/src/10-Core/Wtq/Configuration/OffScreenLocation.cs index 822554c..1b68b24 100644 --- a/src/10-Core/Wtq/Configuration/OffScreenLocation.cs +++ b/src/10-Core/Wtq/Configuration/OffScreenLocation.cs @@ -5,6 +5,11 @@ namespace Wtq.Configuration; /// public enum OffScreenLocation { + /// + /// Used for detecting serialization issues. + /// + None = 0, + /// /// Above the screen. /// @@ -14,4 +19,14 @@ public enum OffScreenLocation /// Below the screen. /// Below, + + /// + /// Left of the screen. + /// + Left, + + /// + /// Right of the screen. + /// + Right, } \ No newline at end of file diff --git a/src/10-Core/Wtq/Configuration/WtqAppOptions.cs b/src/10-Core/Wtq/Configuration/WtqAppOptions.cs index f055227..4d80a42 100644 --- a/src/10-Core/Wtq/Configuration/WtqAppOptions.cs +++ b/src/10-Core/Wtq/Configuration/WtqAppOptions.cs @@ -60,6 +60,9 @@ public sealed class WtqAppOptions /// public TaskBarIconVisibility? TaskbarIconVisibility { get; set; } + /// + public IEnumerable? OffScreenLocations { get; init; } + /// public int? VerticalOffset { get; set; } diff --git a/src/10-Core/Wtq/Configuration/WtqOptions.cs b/src/10-Core/Wtq/Configuration/WtqOptions.cs index e77765a..959fc9e 100644 --- a/src/10-Core/Wtq/Configuration/WtqOptions.cs +++ b/src/10-Core/Wtq/Configuration/WtqOptions.cs @@ -1,3 +1,5 @@ +using static Wtq.Configuration.OffScreenLocation; + namespace Wtq.Configuration; /// @@ -123,6 +125,14 @@ public int AnimationDurationMsWhenSwitchingApps public TaskBarIconVisibility TaskBarIconVisibility { get; init; } = TaskBarIconVisibility.AlwaysHidden; + /// + /// When moving an app off the screen, WTQ looks for an empty space to move the window to.
+ /// Depending on your monitor setup, this may be above the screen, but switches to below if another monitor exists there.
+ /// By default, WTQ looks for empty space in this order: Above, Below, Left, Right. + ///
+ public IEnumerable OffScreenLocations { get; init; } + = [Above, Below, Left, Right]; + /// /// How much room to leave between the top of the terminal and the top of the screen, in pixels.
/// Defaults to "0". @@ -185,6 +195,13 @@ public TaskBarIconVisibility GetTaskbarIconVisibilityForApp(WtqAppOptions opts) return opts.TaskbarIconVisibility ?? TaskBarIconVisibility; } + public IEnumerable GetOffScreenLocationsForApp(WtqAppOptions opts) + { + Guard.Against.Null(opts); + + return opts.OffScreenLocations ?? OffScreenLocations; + } + public float GetVerticalOffsetForApp(WtqAppOptions opts) { Guard.Against.Null(opts); diff --git a/src/10-Core/Wtq/Services/WtqAppToggleService.cs b/src/10-Core/Wtq/Services/WtqAppToggleService.cs index 614dbe4..1fdd5e5 100644 --- a/src/10-Core/Wtq/Services/WtqAppToggleService.cs +++ b/src/10-Core/Wtq/Services/WtqAppToggleService.cs @@ -1,3 +1,5 @@ +using static Wtq.Configuration.OffScreenLocation; + namespace Wtq.Services; /// @@ -20,18 +22,22 @@ public async Task ToggleOnAsync(WtqApp app, ToggleModifiers mods) // Animation duration. var durationMs = GetDurationMs(mods); - // Screen bounds. + // All available screen rects. + var screenRects = await _screenInfoProvider.GetScreenRectsAsync().NoCtx(); + + // Get rect of the screen where the app currently is. var screenRect = await GetTargetScreenRectAsync(app).NoCtx(); - // Source & target bounds. - var windowRectSrc = GetOffScreenWindowRect(app, screenRect); + // Source & target rects. + var windowRectSrc = GetOffScreenWindowRect(app, screenRect, screenRects); var windowRectDst = GetOnScreenWindowRect(app, screenRect); - // Move window. _log.LogDebug("ToggleOn app '{App}' from '{From}' to '{To}'", app, windowRectSrc, windowRectDst); + // Resize window. await app.ResizeWindowAsync(windowRectDst.Size).NoCtx(); + // Move window. await _tween .AnimateAsync( src: windowRectSrc.Location, @@ -50,24 +56,29 @@ public async Task ToggleOffAsync(WtqApp app, ToggleModifiers mods) // Animation duration. var durationMs = GetDurationMs(mods); - // Screen bounds. + // All available screen rects. + var screenRects = await _screenInfoProvider.GetScreenRectsAsync().NoCtx(); + + // Get rect of the screen where the app currently is. var screenRect = await app.GetScreenRectAsync().NoCtx(); - // Source & target bounds. + // Source & target rects. var windowRectSrc = await app.GetWindowRectAsync().NoCtx(); - var windowRectDst = GetOffScreenWindowRect(app, screenRect); + var windowRectDst = GetOffScreenWindowRect(app, screenRect, screenRects); _log.LogDebug("ToggleOff app '{App}' from '{From}' to '{To}'", app, windowRectSrc, windowRectDst); + // Resize window. await app.ResizeWindowAsync(windowRectDst.Size).NoCtx(); + // Move window. await _tween .AnimateAsync( - windowRectSrc.Location, - windowRectDst.Location, - durationMs, - _opts.CurrentValue.AnimationTypeToggleOff, - app.MoveWindowAsync) + src: windowRectSrc.Location, + dst: windowRectDst.Location, + durationMs: durationMs, + animType: _opts.CurrentValue.AnimationTypeToggleOff, + move: app.MoveWindowAsync) .NoCtx(); } @@ -133,18 +144,73 @@ private Rectangle GetOnScreenWindowRect(WtqApp app, Rectangle screenRect) /// /// Get the position rect a window should be when off-screen. /// - private Rectangle GetOffScreenWindowRect(WtqApp app, Rectangle screenRect) + private Rectangle GetOffScreenWindowRect( + WtqApp app, + Rectangle currScreenRect, + Rectangle[] screenRects) + { + Guard.Against.Null(app); + Guard.Against.Null(screenRects); + + // Get the app's current window rectangle. + var windowRect = GetOnScreenWindowRect(app, currScreenRect); + + // Get possible rectangles to move the app to. + var targetRects = GetOffScreenWindowRects(app, windowRect, currScreenRect); + + // Return first target rectangle that does not overlap with a screen. + var targetRect = targetRects + .FirstOrDefault(r => !screenRects.Any(scr => scr.IntersectsWith(r))); + + return !targetRect.IsEmpty ? targetRect : targetRects[0]; + } + + /// + /// Returns a set of s, each a possible off-screen position for the to move to.
+ /// The list is ordered by , as specified in the settings. + ///
+ private Rectangle[] GetOffScreenWindowRects( + WtqApp app, + Rectangle windowRect, + Rectangle screenRect) { Guard.Against.Null(app); + Guard.Against.Null(windowRect); + Guard.Against.Null(screenRect); + + var margin = 100; - var windowRect = GetOnScreenWindowRect(app, screenRect); + return _opts.CurrentValue + .GetOffScreenLocationsForApp(app.Options) + .Select(dir => dir switch + { + Above or None => windowRect with + { + // Top of the screen, minus height of the app window. + Y = screenRect.Y - windowRect.Height - margin, + }, + + Below => windowRect with + { + // Bottom of the screen. + Y = screenRect.Y + screenRect.Height + margin, + }, - windowRect.Y - = screenRect.Y // Top of the screen (which can be negative, when on the non-primary screen). - - windowRect.Height // Minus height of the app window. - - 100; // Minus a little margin. + Left => windowRect with + { + // Left of the screen, minus width of the app window. + X = screenRect.X - windowRect.Width - margin, + }, + + Right => windowRect with + { + // Right of the screen, plus width of the app window. + X = screenRect.X + screenRect.Width + windowRect.Width + margin, + }, - return windowRect; + _ => throw new WtqException("Unknown toggle direction."), + }) + .ToArray(); } /// diff --git a/src/10-Core/Wtq/Utils/WtqTween.cs b/src/10-Core/Wtq/Utils/WtqTween.cs index 6990486..c34b034 100644 --- a/src/10-Core/Wtq/Utils/WtqTween.cs +++ b/src/10-Core/Wtq/Utils/WtqTween.cs @@ -49,11 +49,9 @@ public async Task AnimateAsync( var linearProgress = sinceStartMs / durationMs; var progress = (float)animFunc(linearProgress); - var current = MathUtils.Lerp(src, dst, progress); - // TODO: Currently, we don't need to move on the X-axis, and there's a little bit of jitter, where X seems to lerp around a bit. - // Find out why and fix that, then remove this. - current.X = dst.X; + // Find out why and fix that. + var current = MathUtils.Lerp(src, dst, progress); await move(current).NoCtx();