// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osuTK; namespace osu.Game.Graphics.Containers { /// /// Handles user-defined scaling, allowing application at multiple levels defined by . /// public class ScalingContainer : Container { private Bindable sizeX; private Bindable sizeY; private Bindable posX; private Bindable posY; private readonly ScalingMode? targetMode; private Bindable scalingMode; private readonly Container content; protected override Container Content => content; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; private readonly Container sizableContainer; private BackgroundScreenStack backgroundStack; private bool allowScaling = true; /// /// Whether user scaling preferences should be applied. Enabled by default. /// public bool AllowScaling { get => allowScaling; set { if (value == allowScaling) return; allowScaling = value; if (IsLoaded) updateSize(); } } /// /// Create a new instance. /// /// The mode which this container should be handling. Handles all modes if null. public ScalingContainer(ScalingMode? targetMode = null) { this.targetMode = targetMode; RelativeSizeAxes = Axes.Both; InternalChild = sizableContainer = new AlwaysInputContainer { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, CornerRadius = 10, Child = content = new ScalingDrawSizePreservingFillContainer(targetMode != ScalingMode.Gameplay) }; } private class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer { private readonly bool applyUIScale; private Bindable uiScale; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public ScalingDrawSizePreservingFillContainer(bool applyUIScale) { this.applyUIScale = applyUIScale; } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { if (applyUIScale) { uiScale = osuConfig.GetBindable(OsuSetting.UIScale); uiScale.BindValueChanged(scaleChanged, true); } } private void scaleChanged(ValueChangedEvent args) { this.ScaleTo(new Vector2(args.NewValue), 500, Easing.Out); this.ResizeTo(new Vector2(1 / args.NewValue), 500, Easing.Out); } } [Resolved] private ISafeArea safeArea { get; set; } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { scalingMode = config.GetBindable(OsuSetting.Scaling); scalingMode.ValueChanged += _ => updateSize(); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); sizeX.ValueChanged += _ => updateSize(); sizeY = config.GetBindable(OsuSetting.ScalingSizeY); sizeY.ValueChanged += _ => updateSize(); posX = config.GetBindable(OsuSetting.ScalingPositionX); posX.ValueChanged += _ => updateSize(); posY = config.GetBindable(OsuSetting.ScalingPositionY); posY.ValueChanged += _ => updateSize(); safeArea.SafeAreaPadding.BindValueChanged(_ => updateSize()); } protected override void LoadComplete() { base.LoadComplete(); updateSize(); sizableContainer.FinishTransforms(); } private bool requiresBackgroundVisible => (scalingMode.Value == ScalingMode.Everything || scalingMode.Value == ScalingMode.ExcludeOverlays) && (sizeX.Value != 1 || sizeY.Value != 1); private void updateSize() { const float fade_time = 500; if (targetMode == ScalingMode.Everything) { // the top level scaling container manages the background to be displayed while scaling. if (requiresBackgroundVisible) { if (backgroundStack == null) { AddInternal(backgroundStack = new BackgroundScreenStack { Colour = OsuColour.Gray(0.1f), Alpha = 0, Depth = float.MaxValue }); backgroundStack.Push(new ScalingBackgroundScreen()); } backgroundStack.FadeIn(fade_time); } else backgroundStack?.FadeOut(fade_time); } bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode); var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; bool requiresMasking = scaling && targetSize != Vector2.One // 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. || (targetMode == ScalingMode.Everything && safeArea.SafeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart); sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); } private class ScalingBackgroundScreen : BackgroundScreenDefault { protected override bool AllowStoryboardBackground => false; public override void OnEntering(IScreen last) { this.FadeInFromZero(4000, Easing.OutQuint); } } private class AlwaysInputContainer : Container { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public AlwaysInputContainer() { RelativeSizeAxes = Axes.Both; } } } }