diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 514538161e..ec6e962c6a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -111,6 +111,33 @@ public void TestIncompatibilityToggling() && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror))); } + [Test] + public void TestDimmedState() + { + createScreen(); + changeRuleset(0); + + AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value)); + + ModColumn lastColumn = null; + + AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value); + AddStep("request scroll to last column", () => + { + var lastDimContainer = this.ChildrenOfType().Last(); + lastColumn = lastDimContainer.Column; + lastDimContainer.RequestScroll?.Invoke(lastDimContainer); + }); + AddUntilStep("column undimmed", () => lastColumn.Active.Value); + + AddStep("click panel", () => + { + InputManager.MoveMouseTo(lastColumn.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("panel selected", () => lastColumn.ChildrenOfType().First().Active.Value); + } + [Test] public void TestCustomisationToggleState() { diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index 017ea6ec32..817b8409e6 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -57,6 +58,26 @@ public OsuScrollContainer(Direction scrollDirection = Direction.Vertical) { } + /// + /// Scrolls a into view. + /// + /// The to scroll into view. + /// Whether to animate the movement. + /// An added amount to scroll beyond the requirement to bring the target into view. + public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0) + { + float childPos0 = GetChildPosInContent(d); + float childPos1 = GetChildPosInContent(d, d.DrawSize); + + float minPos = Math.Min(childPos0, childPos1); + float maxPos = Math.Max(childPos0, childPos1); + + if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) + ScrollTo(minPos - extraScroll, animated); + else if (maxPos > Current + DisplayableContent) + ScrollTo(maxPos - DisplayableContent + extraScroll, animated); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (shouldPerformRightMouseScroll(e)) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 6a2c2adcba..018922c074 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -53,6 +53,9 @@ public Func? Filter } public Bindable> SelectedMods = new Bindable>(Array.Empty()); + public Bindable Active = new BindableBool(true); + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 8a83071109..ffd6e9a52c 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -13,7 +13,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; @@ -59,7 +61,8 @@ public Func IsValidMod private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; - private FillFlowContainer columnFlow = null!; + private ColumnScrollContainer columnScroll = null!; + private ColumnFlowContainer columnFlow = null!; [BackgroundDependencyLoader] private void load() @@ -95,27 +98,27 @@ private void load() RelativePositionAxes = Axes.Both, Children = new Drawable[] { - new OsuScrollContainer(Direction.Horizontal) + columnScroll = new ColumnScrollContainer { RelativeSizeAxes = Axes.Both, Masking = false, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = columnFlow = new ModColumnContainer + Child = columnFlow = new ColumnFlowContainer { Direction = FillDirection.Horizontal, Shear = new Vector2(SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Right = 70 }, + Margin = new MarginPadding { Horizontal = 70 }, Children = new[] { - CreateModColumn(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), - CreateModColumn(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), - CreateModColumn(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), - CreateModColumn(ModType.Conversion), - CreateModColumn(ModType.Fun) + createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), + createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), + createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), + createModColumnContent(ModType.Conversion), + createModColumnContent(ModType.Fun) } } } @@ -153,6 +156,14 @@ private void load() } } + private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) + => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -166,7 +177,7 @@ protected override void LoadComplete() updateSelectionFromBindable(); }, true); - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) { column.SelectedMods.BindValueChanged(updateBindableFromSelection); } @@ -191,7 +202,7 @@ private void updateMultiplier() private void updateAvailableMods() { - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) column.Filter = isValidMod; } @@ -244,7 +255,7 @@ private void updateSelectionFromBindable() // to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods, // and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods. // selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call. - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); } @@ -265,7 +276,7 @@ private void updateBindableFromSelection(ValueChangedEvent> m } protected virtual IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) - => columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray(); + => columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray(); protected override void PopIn() { @@ -280,7 +291,8 @@ protected override void PopIn() for (int i = 0; i < columnFlow.Count; i++) { - columnFlow[i].TopLevelContent + columnFlow[i].Column + .TopLevelContent .Delay(i * 30) .MoveToY(0, fade_in_duration, Easing.OutQuint) .FadeIn(fade_in_duration, Easing.OutQuint); @@ -301,27 +313,68 @@ protected override void PopOut() { const float distance = 700; - columnFlow[i].TopLevelContent + columnFlow[i].Column + .TopLevelContent .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) .FadeOut(fade_out_duration, Easing.OutQuint); } } - private class ModColumnContainer : FillFlowContainer + internal class ColumnScrollContainer : OsuScrollContainer { + public ColumnScrollContainer() + : base(Direction.Horizontal) + { + } + + protected override void Update() + { + base.Update(); + + // the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space. + // note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns. + float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent); + float rightVisibleBound = leftVisibleBound + DrawWidth; + + // if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass. + // this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past. + float leftMovementBound = Math.Min(Current, Target); + float rightMovementBound = Math.Max(Current, Target) + DrawWidth; + + foreach (var column in Child) + { + // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, + // so we have to manually compensate. + var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent); + + bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) + && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); + bool isBeingScrolledToward = Precision.AlmostBigger(topLeft.X, leftMovementBound) + && Precision.DefinitelyBigger(rightMovementBound, bottomRight.X); + + column.Active.Value = isCurrentlyVisible || isBeingScrolledToward; + } + } + } + + internal class ColumnFlowContainer : FillFlowContainer + { + public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); - public ModColumnContainer() + public ColumnFlowContainer() { AddLayout(drawSizeLayout); } - public override void Add(ModColumn column) + public override void Add(ColumnDimContainer dimContainer) { - base.Add(column); + base.Add(dimContainer); - Debug.Assert(column != null); - column.Shear = Vector2.Zero; + Debug.Assert(dimContainer != null); + dimContainer.Column.Shear = Vector2.Zero; } protected override void Update() @@ -341,6 +394,63 @@ protected override void Update() } } + internal class ColumnDimContainer : Container + { + public ModColumn Column { get; } + + public readonly Bindable Active = new BindableBool(); + public Action? RequestScroll { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ColumnDimContainer(ModColumn column) + { + Child = Column = column; + column.Active.BindTo(Active); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Active.BindValueChanged(_ => updateDim(), true); + FinishTransforms(); + } + + private void updateDim() + { + Colour4 targetColour; + + if (Active.Value) + targetColour = Colour4.White; + else + targetColour = IsHovered ? colours.GrayC : colours.Gray8; + + this.FadeColour(targetColour, 800, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Active.Value) + RequestScroll?.Invoke(this); + + return true; + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + updateDim(); + return Active.Value; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateDim(); + } + } + private class ClickToReturnContainer : Container { public BindableBool HandleMouse { get; } = new BindableBool(); diff --git a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs index aba47d5423..d27f97f3d2 100644 --- a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs +++ b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs @@ -14,13 +14,13 @@ namespace osu.Game.Overlays.Mods /// public class NestedVerticalScrollContainer : OsuScrollContainer { - private OsuScrollContainer? parentScrollContainer; + private ModSelectScreen.ColumnScrollContainer? parentScrollContainer; protected override void LoadComplete() { base.LoadComplete(); - parentScrollContainer = this.FindClosestParent(); + parentScrollContainer = this.FindClosestParent(); } protected override bool OnScroll(ScrollEvent e)