2019-01-24 08:43:03 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2019-01-04 04:29:37 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
2019-01-04 04:29:37 +00:00
using osu.Framework.Allocation ;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables ;
2019-01-04 04:29:37 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2022-03-02 11:04:53 +00:00
using osu.Framework.Graphics.Primitives ;
2022-03-11 13:04:12 +00:00
using osu.Framework.Layout ;
using osu.Framework.Platform ;
2019-03-25 04:28:51 +00:00
using osu.Framework.Screens ;
2019-01-04 04:29:37 +00:00
using osu.Game.Configuration ;
2019-03-24 21:49:57 +00:00
using osu.Game.Screens ;
using osu.Game.Screens.Backgrounds ;
2019-01-04 04:29:37 +00:00
using osuTK ;
2023-05-02 05:33:57 +00:00
using osuTK.Graphics ;
2019-01-04 04:29:37 +00:00
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// Handles user-defined scaling, allowing application at multiple levels defined by <see cref="ScalingMode"/>.
/// </summary>
public partial class ScalingContainer : Container
{
2022-06-06 09:06:46 +00:00
internal const float TRANSITION_DURATION = 500 ;
2022-04-14 09:58:20 +00:00
2019-01-04 04:29:37 +00:00
private Bindable < float > sizeX ;
private Bindable < float > sizeY ;
private Bindable < float > posX ;
private Bindable < float > posY ;
2022-10-28 04:52:45 +00:00
private Bindable < bool > applySafeAreaPadding ;
2019-01-04 04:29:37 +00:00
2022-02-04 10:19:44 +00:00
private Bindable < MarginPadding > safeAreaPadding ;
2019-01-04 06:28:35 +00:00
private readonly ScalingMode ? targetMode ;
2019-01-04 04:29:37 +00:00
private Bindable < ScalingMode > scalingMode ;
private readonly Container content ;
protected override Container < Drawable > Content = > content ;
2019-01-16 08:21:26 +00:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > true ;
2019-01-04 04:29:37 +00:00
private readonly Container sizableContainer ;
2019-03-25 04:28:51 +00:00
private BackgroundScreenStack backgroundStack ;
2019-01-04 04:29:37 +00:00
2023-03-28 13:24:05 +00:00
private Bindable < float > scalingMenuBackgroundDim ;
2022-03-02 11:33:28 +00:00
private RectangleF ? customRect ;
private bool customRectIsRelativePosition ;
2021-04-29 08:19:47 +00:00
/// <summary>
2022-03-02 11:04:53 +00:00
/// Set a custom position and scale which overrides any user specification.
2021-04-29 08:19:47 +00:00
/// </summary>
2022-03-03 05:38:20 +00:00
/// <param name="rect">A rectangle with positional and sizing information for this container to conform to. <c>null</c> will clear the custom rect and revert to user settings.</param>
/// <param name="relativePosition">Whether the position portion of the provided rect is in relative coordinate space or not.</param>
2022-03-02 11:33:28 +00:00
public void SetCustomRect ( RectangleF ? rect , bool relativePosition = false )
2021-04-29 08:19:47 +00:00
{
2022-03-02 11:33:28 +00:00
customRect = rect ;
customRectIsRelativePosition = relativePosition ;
2021-04-29 08:19:47 +00:00
2022-03-02 11:04:53 +00:00
if ( IsLoaded ) Scheduler . AddOnce ( updateSize ) ;
2021-04-29 08:19:47 +00:00
}
2022-03-02 10:41:47 +00:00
private const float corner_radius = 10 ;
2019-01-04 04:29:37 +00:00
/// <summary>
/// Create a new instance.
/// </summary>
2019-01-04 06:28:35 +00:00
/// <param name="targetMode">The mode which this container should be handling. Handles all modes if null.</param>
public ScalingContainer ( ScalingMode ? targetMode = null )
2019-01-04 04:29:37 +00:00
{
this . targetMode = targetMode ;
RelativeSizeAxes = Axes . Both ;
2022-03-11 13:04:12 +00:00
InternalChild = sizableContainer = new SizeableAlwaysInputContainer ( targetMode = = ScalingMode . Everything )
2019-01-04 04:29:37 +00:00
{
RelativeSizeAxes = Axes . Both ,
RelativePositionAxes = Axes . Both ,
2022-03-02 10:41:47 +00:00
CornerRadius = corner_radius ,
2019-01-09 10:01:33 +00:00
Child = content = new ScalingDrawSizePreservingFillContainer ( targetMode ! = ScalingMode . Gameplay )
2019-01-04 04:29:37 +00:00
} ;
}
2022-04-19 07:37:38 +00:00
public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer
2019-01-09 10:01:33 +00:00
{
private readonly bool applyUIScale ;
private Bindable < float > uiScale ;
2022-04-19 07:37:38 +00:00
protected float CurrentScale { get ; private set ; } = 1 ;
2022-04-14 09:58:20 +00:00
2019-01-16 08:21:26 +00:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > true ;
2019-01-09 10:01:33 +00:00
public ScalingDrawSizePreservingFillContainer ( bool applyUIScale )
{
this . applyUIScale = applyUIScale ;
}
[BackgroundDependencyLoader]
private void load ( OsuConfigManager osuConfig )
{
if ( applyUIScale )
{
uiScale = osuConfig . GetBindable < float > ( OsuSetting . UIScale ) ;
2022-06-06 09:06:46 +00:00
uiScale . BindValueChanged ( args = > this . TransformTo ( nameof ( CurrentScale ) , args . NewValue , TRANSITION_DURATION , Easing . OutQuart ) , true ) ;
2019-01-09 10:01:33 +00:00
}
}
2022-04-14 09:58:20 +00:00
protected override void Update ( )
{
2022-04-19 07:37:38 +00:00
Scale = new Vector2 ( CurrentScale ) ;
Size = new Vector2 ( 1 / CurrentScale ) ;
2022-04-14 09:58:20 +00:00
base . Update ( ) ;
2019-01-09 10:01:33 +00:00
}
}
2019-01-04 04:29:37 +00:00
[BackgroundDependencyLoader]
2022-02-04 10:19:44 +00:00
private void load ( OsuConfigManager config , ISafeArea safeArea )
2019-01-04 04:29:37 +00:00
{
scalingMode = config . GetBindable < ScalingMode > ( OsuSetting . Scaling ) ;
2022-02-04 11:33:15 +00:00
scalingMode . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2019-01-04 04:29:37 +00:00
sizeX = config . GetBindable < float > ( OsuSetting . ScalingSizeX ) ;
2022-02-04 11:33:15 +00:00
sizeX . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2019-01-04 04:29:37 +00:00
sizeY = config . GetBindable < float > ( OsuSetting . ScalingSizeY ) ;
2022-02-04 11:33:15 +00:00
sizeY . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2019-01-04 04:29:37 +00:00
posX = config . GetBindable < float > ( OsuSetting . ScalingPositionX ) ;
2022-02-04 11:33:15 +00:00
posX . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2019-01-04 04:29:37 +00:00
posY = config . GetBindable < float > ( OsuSetting . ScalingPositionY ) ;
2022-02-04 11:33:15 +00:00
posY . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2022-02-04 07:07:05 +00:00
2022-10-28 04:52:45 +00:00
applySafeAreaPadding = config . GetBindable < bool > ( OsuSetting . SafeAreaConsiderations ) ;
applySafeAreaPadding . BindValueChanged ( _ = > Scheduler . AddOnce ( updateSize ) ) ;
2022-02-04 10:19:44 +00:00
safeAreaPadding = safeArea . SafeAreaPadding . GetBoundCopy ( ) ;
2022-02-04 11:33:15 +00:00
safeAreaPadding . BindValueChanged ( _ = > Scheduler . AddOnce ( updateSize ) ) ;
2023-03-28 13:24:05 +00:00
2023-03-29 13:55:25 +00:00
scalingMenuBackgroundDim = config . GetBindable < float > ( OsuSetting . ScalingBackgroundDim ) ;
2023-03-28 13:24:05 +00:00
scalingMenuBackgroundDim . ValueChanged + = _ = > Scheduler . AddOnce ( updateSize ) ;
2019-01-04 04:29:37 +00:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-02-04 13:31:41 +00:00
updateSize ( ) ;
2019-01-04 06:28:35 +00:00
sizableContainer . FinishTransforms ( ) ;
2019-01-04 04:29:37 +00:00
}
2023-03-28 13:24:05 +00:00
private bool requiresBackgroundVisible = > ( scalingMode . Value = = ScalingMode . Everything | | scalingMode . Value = = ScalingMode . ExcludeOverlays )
& & ( sizeX . Value ! = 1 | | sizeY . Value ! = 1 )
2023-05-02 05:31:39 +00:00
& & scalingMenuBackgroundDim . Value < 1 ;
2019-01-04 04:29:37 +00:00
private void updateSize ( )
{
if ( targetMode = = ScalingMode . Everything )
{
// the top level scaling container manages the background to be displayed while scaling.
if ( requiresBackgroundVisible )
{
2019-03-25 04:28:51 +00:00
if ( backgroundStack = = null )
{
AddInternal ( backgroundStack = new BackgroundScreenStack
2019-01-04 04:29:37 +00:00
{
Alpha = 0 ,
2023-05-02 05:33:57 +00:00
Colour = Color4 . Black ,
2019-01-04 04:29:37 +00:00
Depth = float . MaxValue
} ) ;
2019-03-25 04:28:51 +00:00
2019-03-25 04:38:50 +00:00
backgroundStack . Push ( new ScalingBackgroundScreen ( ) ) ;
2019-03-25 04:28:51 +00:00
}
2022-06-06 09:06:46 +00:00
backgroundStack . FadeIn ( TRANSITION_DURATION ) ;
2023-03-29 13:56:35 +00:00
backgroundStack . FadeColour ( OsuColour . Gray ( 1.0f - scalingMenuBackgroundDim . Value ) , TRANSITION_DURATION , Easing . OutQuint ) ;
2019-01-04 04:29:37 +00:00
}
else
2022-06-06 09:06:46 +00:00
backgroundStack ? . FadeOut ( TRANSITION_DURATION ) ;
2019-01-04 04:29:37 +00:00
}
2022-03-02 11:33:28 +00:00
RectangleF targetRect = new RectangleF ( Vector2 . Zero , Vector2 . One ) ;
2022-03-02 11:04:53 +00:00
2022-03-02 11:33:28 +00:00
if ( customRect ! = null )
2022-03-02 11:04:53 +00:00
{
2022-03-02 11:33:28 +00:00
sizableContainer . RelativePositionAxes = customRectIsRelativePosition ? Axes . Both : Axes . None ;
2022-03-02 11:04:53 +00:00
2022-03-02 11:33:28 +00:00
targetRect = customRect . Value ;
2022-03-02 11:04:53 +00:00
}
else if ( targetMode = = null | | scalingMode . Value = = targetMode )
{
sizableContainer . RelativePositionAxes = Axes . Both ;
Vector2 scale = new Vector2 ( sizeX . Value , sizeY . Value ) ;
Vector2 pos = new Vector2 ( posX . Value , posY . Value ) * ( Vector2 . One - scale ) ;
2022-03-02 11:33:28 +00:00
targetRect = new RectangleF ( pos , scale ) ;
2022-03-02 11:04:53 +00:00
}
2019-01-04 04:29:37 +00:00
2022-03-02 11:33:28 +00:00
bool requiresMasking = targetRect . Size ! = Vector2 . One
2022-02-04 07:07:05 +00:00
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
2022-10-28 04:52:45 +00:00
| | ( targetMode = = ScalingMode . Everything & & ( applySafeAreaPadding . Value & & safeAreaPadding . Value . Total ! = Vector2 . Zero ) ) ;
2019-01-04 04:29:37 +00:00
if ( requiresMasking )
sizableContainer . Masking = true ;
2022-06-06 09:06:46 +00:00
sizableContainer . MoveTo ( targetRect . Location , TRANSITION_DURATION , Easing . OutQuart ) ;
sizableContainer . ResizeTo ( targetRect . Size , TRANSITION_DURATION , Easing . OutQuart ) ;
2022-03-02 11:25:34 +00:00
2022-03-03 05:35:52 +00:00
// Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
// Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
// An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
2022-06-06 09:06:46 +00:00
sizableContainer . TransformTo ( nameof ( CornerRadius ) , requiresMasking ? corner_radius : 0 , TRANSITION_DURATION , requiresMasking ? Easing . OutQuart : Easing . None )
2022-03-02 11:25:34 +00:00
. OnComplete ( _ = > { sizableContainer . Masking = requiresMasking ; } ) ;
2019-01-04 04:29:37 +00:00
}
2019-01-16 08:21:26 +00:00
2019-03-25 04:38:50 +00:00
private partial class ScalingBackgroundScreen : BackgroundScreenDefault
2019-03-25 04:28:51 +00:00
{
2021-06-03 05:24:21 +00:00
protected override bool AllowStoryboardBackground = > false ;
2022-04-21 15:52:44 +00:00
public override void OnEntering ( ScreenTransitionEvent e )
2019-03-25 04:28:51 +00:00
{
this . FadeInFromZero ( 4000 , Easing . OutQuint ) ;
}
}
2022-03-11 13:04:12 +00:00
private partial class SizeableAlwaysInputContainer : Container
2019-01-16 08:21:26 +00:00
{
2022-03-11 13:04:12 +00:00
[Resolved]
private GameHost host { get ; set ; }
[Resolved]
private ISafeArea safeArea { get ; set ; }
2022-10-28 04:52:45 +00:00
[Resolved]
private OsuConfigManager config { get ; set ; }
2022-03-11 13:04:12 +00:00
private readonly bool confineHostCursor ;
private readonly LayoutValue cursorRectCache = new LayoutValue ( Invalidation . RequiredParentSizeToFit ) ;
2019-01-16 08:21:26 +00:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > true ;
2022-03-11 13:04:12 +00:00
/// <summary>
/// Container used for sizing/positioning purposes in <see cref="ScalingContainer"/>. Always receives mouse input.
/// </summary>
/// <param name="confineHostCursor">Whether to confine the host cursor to the draw area of this container.</param>
/// <remarks>Cursor confinement will abide by the <see cref="OsuSetting.ConfineMouseMode"/> setting.</remarks>
public SizeableAlwaysInputContainer ( bool confineHostCursor )
2019-01-16 08:21:26 +00:00
{
RelativeSizeAxes = Axes . Both ;
2022-03-11 13:04:12 +00:00
this . confineHostCursor = confineHostCursor ;
if ( confineHostCursor )
AddLayout ( cursorRectCache ) ;
}
protected override void Update ( )
{
base . Update ( ) ;
if ( confineHostCursor & & ! cursorRectCache . IsValid )
{
updateHostCursorConfineRect ( ) ;
cursorRectCache . Validate ( ) ;
}
}
private void updateHostCursorConfineRect ( )
{
if ( host . Window = = null ) return ;
2022-10-28 04:52:45 +00:00
bool coversWholeScreen = Size = = Vector2 . One & & ( ! config . Get < bool > ( OsuSetting . SafeAreaConsiderations ) | | safeArea . SafeAreaPadding . Value . Total = = Vector2 . Zero ) ;
2022-06-24 05:48:43 +00:00
host . Window . CursorConfineRect = coversWholeScreen ? null : ToScreenSpace ( DrawRectangle ) . AABBFloat ;
2019-01-16 08:21:26 +00:00
}
}
2019-01-04 04:29:37 +00:00
}
}