diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 194e4301c232..30cf37062148 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -16,6 +16,8 @@ using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections.Input; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Tests.Visual.Settings @@ -117,12 +119,31 @@ public void TestRotationValidity() } [Test] - public void TestOffsetValidity() + public void TestOffsetClamping() { ensureValid(); - AddStep("move right", () => tabletHandler.AreaOffset.Value = Vector2.Zero); + AddStep("move outside bounds", () => tabletHandler.AreaOffset.Value = Vector2.Zero); + AddAssert("offset is clamped", () => tabletHandler.AreaOffset.Value == tabletHandler.AreaSize.Value / 2); + ensureValid(); + + AddStep("disable lock to usable area", () => settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value = false); + AddStep("move outside bounds", () => tabletHandler.AreaOffset.Value = Vector2.Zero); ensureInvalid(); - AddStep("move back", () => tabletHandler.AreaOffset.Value = tabletHandler.AreaSize.Value / 2); + + AddStep("enable lock to usable area", () => settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value = true); + AddAssert("offset is clamped again", () => tabletHandler.AreaOffset.Value == tabletHandler.AreaSize.Value / 2); + ensureValid(); + + AddStep("rotate 45", () => tabletHandler.Rotation.Value = 45); + AddAssert("lock is disabled automatically", () => !settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value); + ensureInvalid(); + + AddStep("attempt to enable lock", () => settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value = true); + AddAssert("lock remains disabled", () => !settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value); + + AddStep("rotate to 0", () => tabletHandler.Rotation.Value = 0); + AddStep("enable lock again", () => settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value = true); + AddAssert("lock enabled successfully", () => settings.ChildrenOfType().First(c => c.Caption == TabletSettingsStrings.LockToUsableArea).Current.Value); ensureValid(); } diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index ff0ced457fc1..88a52961adfd 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.cs @@ -34,6 +34,11 @@ public static class TabletSettingsStrings /// public static LocalisableString ConformToCurrentGameAspectRatio => new TranslatableString(getKey(@"conform_to_current_game_aspect_ratio"), @"Conform to current game aspect ratio"); + /// + /// "Lock to usable area" + /// + public static LocalisableString LockToUsableArea => new TranslatableString(getKey(@"lock_to_usable_area"), @"Lock to usable area"); + /// /// "X Offset" /// diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index 5cd09265509b..5a0837817528 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -24,6 +24,8 @@ public partial class TabletAreaSelection : CompositeDrawable { public bool IsWithinBounds { get; private set; } + public readonly BindableBool LockToUsableArea = new BindableBool(true); + private readonly ITabletHandler handler; private Container tabletContainer; @@ -84,7 +86,7 @@ private void load(OverlayColourProvider colourProvider) RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background4, }, - usableAreaContainer = new UsableAreaContainer(handler) + usableAreaContainer = new UsableAreaContainer(handler, LockToUsableArea) { Origin = Anchor.Centre, Children = new Drawable[] @@ -246,10 +248,14 @@ protected override void Update() public partial class UsableAreaContainer : Container { + private readonly ITabletHandler tabletHandler; private readonly Bindable areaOffset; + private readonly BindableBool lockToUsableArea; - public UsableAreaContainer(ITabletHandler tabletHandler) + public UsableAreaContainer(ITabletHandler tabletHandler, BindableBool lockToUsableArea) { + this.tabletHandler = tabletHandler; + this.lockToUsableArea = lockToUsableArea; areaOffset = tabletHandler.AreaOffset.GetBoundCopy(); } @@ -258,7 +264,8 @@ public UsableAreaContainer(ITabletHandler tabletHandler) protected override void OnDrag(DragEvent e) { var newPos = Position + e.Delta; - this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent!.Size)); + newPos = lockToUsableArea.Value ? tabletHandler.ClampOffset(newPos) : Vector2.Clamp(newPos, Vector2.Zero, Parent!.Size); + this.MoveTo(newPos); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletHandlerExtensions.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletHandlerExtensions.cs new file mode 100644 index 000000000000..7acb1669fc27 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletHandlerExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Input.Handlers.Tablet; +using osuTK; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public static class TabletHandlerExtensions + { + public static Vector2 ClampOffset(this ITabletHandler handler, Vector2 offset) + { + if (handler.Tablet.Value == null) + return offset; + + var size = handler.AreaSize.Value; + var tabletSize = handler.Tablet.Value.Size; + + float rad = float.DegreesToRadians(handler.Rotation.Value); + float cos = MathF.Abs(MathF.Cos(rad)); + float sin = MathF.Abs(MathF.Sin(rad)); + + float maxX = (size.X / 2) * cos + (size.Y / 2) * sin; + float maxY = (size.X / 2) * sin + (size.Y / 2) * cos; + + float minX = MathF.Min(maxX, tabletSize.X / 2); + float maxXRange = MathF.Max(tabletSize.X - maxX, tabletSize.X / 2); + float minY = MathF.Min(maxY, tabletSize.Y / 2); + float maxYRange = MathF.Max(tabletSize.Y - maxY, tabletSize.Y / 2); + + return new Vector2( + Math.Clamp(offset.X, minX, maxXRange), + Math.Clamp(offset.Y, minY, maxYRange) + ); + } + + public static bool CanFit(this ITabletHandler handler) + { + if (handler.Tablet.Value == null) + return true; + + var size = handler.AreaSize.Value; + var tabletSize = handler.Tablet.Value.Size; + + float rad = float.DegreesToRadians(handler.Rotation.Value); + float cos = MathF.Abs(MathF.Cos(rad)); + float sin = MathF.Abs(MathF.Sin(rad)); + + float maxX = (size.X / 2) * cos + (size.Y / 2) * sin; + float maxY = (size.X / 2) * sin + (size.Y / 2) * cos; + + const float lenience = 0.5f; + return maxX <= tabletSize.X / 2 + lenience && maxY <= tabletSize.Y / 2 + lenience; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index aeae81a99814..02e080e2e922 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -72,6 +72,7 @@ public partial class TabletSettings : InputSubsection }; private readonly BindableBool aspectLock = new BindableBool(); + private readonly BindableBool lockToUsableArea = new BindableBool(true); private ScheduledDelegate aspectRatioApplication; @@ -161,6 +162,11 @@ private void load(OsuColour colours, LocalisationManager localisation, OsuConfig { Padding = SettingsPanel.CONTENT_PADDING, }, + new SettingsItemV2(new FormCheckBox + { + Caption = TabletSettingsStrings.LockToUsableArea, + Current = lockToUsableArea, + }), new SettingsItemV2(new FormSliderBar { TransferValueOnCommit = true, @@ -200,14 +206,56 @@ protected override void LoadComplete() { base.LoadComplete(); + AreaSelection.LockToUsableArea.BindTo(lockToUsableArea); + + lockToUsableArea.BindValueChanged(val => Schedule(() => + { + if (val.NewValue) + { + if (tabletHandler.CanFit()) + { + var clampedOffset = clampOffset(areaOffset.Value); + if (clampedOffset != areaOffset.Value) + areaOffset.Value = clampedOffset; + } + else + { + lockToUsableArea.Value = false; + } + } + })); + enabled.BindTo(tabletHandler.Enabled); enabled.BindValueChanged(_ => Scheduler.AddOnce(updateVisibility)); rotation.BindTo(tabletHandler.Rotation); + rotation.BindValueChanged(val => Schedule(() => + { + if (lockToUsableArea.Value) + { + if (tabletHandler.CanFit()) + { + var clampedOffset = clampOffset(areaOffset.Value); + if (clampedOffset != areaOffset.Value) + areaOffset.Value = clampedOffset; + } + else + { + lockToUsableArea.Value = false; + } + } + }), true); areaOffset.BindTo(tabletHandler.AreaOffset); areaOffset.BindValueChanged(val => Schedule(() => { + var clamped = clampOffset(val.NewValue); + if (clamped != val.NewValue) + { + areaOffset.Value = clamped; + return; + } + offsetX.Value = val.NewValue.X; offsetY.Value = val.NewValue.Y; }), true); @@ -218,6 +266,20 @@ protected override void LoadComplete() areaSize.BindTo(tabletHandler.AreaSize); areaSize.BindValueChanged(val => Schedule(() => { + if (lockToUsableArea.Value) + { + if (tabletHandler.CanFit()) + { + var clampedOffset = clampOffset(areaOffset.Value); + if (clampedOffset != areaOffset.Value) + areaOffset.Value = clampedOffset; + } + else + { + lockToUsableArea.Value = false; + } + } + sizeX.Value = val.NewValue.X; sizeY.Value = val.NewValue.Y; }), true); @@ -335,6 +397,14 @@ private void forceAspectRatio(float aspectRatio) aspectLock.Value = true; } + private Vector2 clampOffset(Vector2 offset) + { + if (!lockToUsableArea.Value) + return offset; + + return tabletHandler.ClampOffset(offset); + } + private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio; private float currentAspectRatio => sizeX.Value / sizeY.Value;