1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-16 10:17:36 +08:00
osu-lazer/osu.Game/Overlays/Mods/ModSelectOverlay.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

794 lines
30 KiB
C#
Raw Normal View History

2022-03-27 05:43:17 +08: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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
2022-07-01 19:43:12 +08:00
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2022-03-27 05:43:17 +08:00
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
2022-03-27 05:43:17 +08:00
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
2022-03-27 05:43:17 +08:00
using osu.Game.Configuration;
using osu.Game.Graphics;
2022-03-27 05:43:17 +08:00
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
2022-03-27 05:43:17 +08:00
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
2022-03-27 05:43:17 +08:00
using osu.Game.Rulesets.Mods;
2022-05-12 01:02:45 +08:00
using osu.Game.Utils;
2022-03-27 05:43:17 +08:00
using osuTK;
namespace osu.Game.Overlays.Mods
{
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler
2022-03-27 05:43:17 +08:00
{
public const int BUTTON_WIDTH = 200;
2022-07-01 19:43:12 +08:00
protected override string PopInSampleName => "";
protected override string PopOutSampleName => @"SongSelect/mod-select-overlay-pop-out";
2022-03-27 05:43:17 +08:00
[Cached]
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
2022-03-27 05:43:17 +08:00
/// <summary>
/// Contains a dictionary with the current <see cref="ModState"/> of all mods applicable for the current ruleset.
/// </summary>
/// <remarks>
/// Contrary to <see cref="OsuGameBase.AvailableMods"/> and <see cref="globalAvailableMods"/>, the <see cref="Mod"/> instances
/// inside the <see cref="ModState"/> objects are owned solely by this <see cref="ModSelectOverlay"/> instance.
/// </remarks>
public Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> AvailableMods { get; } = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(new Dictionary<ModType, IReadOnlyList<ModState>>());
2022-06-24 20:25:23 +08:00
private Func<Mod, bool> isValidMod = _ => true;
2022-05-08 00:49:29 +08:00
/// <summary>
/// A function determining whether each mod in the column should be displayed.
/// A return value of <see langword="true"/> means that the mod is not filtered and therefore its corresponding panel should be displayed.
/// A return value of <see langword="false"/> means that the mod is filtered out and therefore its corresponding panel should be hidden.
/// </summary>
public Func<Mod, bool> IsValidMod
{
get => isValidMod;
set
{
isValidMod = value ?? throw new ArgumentNullException(nameof(value));
2022-05-12 01:02:45 +08:00
filterMods();
}
}
/// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowTotalMultiplier => true;
/// <summary>
/// Whether per-mod customisation controls are visible.
/// </summary>
protected virtual bool AllowCustomisation => true;
/// <summary>
/// Whether the column with available mod presets should be shown.
/// </summary>
protected virtual bool ShowPresets => false;
protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false);
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
protected virtual IEnumerable<ShearedButton> CreateFooterButtons()
{
if (AllowCustomisation)
{
yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH)
{
Text = ModSelectOverlayStrings.ModCustomisation,
Active = { BindTarget = customisationVisible }
};
}
yield return new DeselectAllModsButton(this);
}
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
private IEnumerable<ModState> allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value);
2022-05-12 01:02:45 +08:00
2022-03-27 05:43:17 +08:00
private readonly BindableBool customisationVisible = new BindableBool();
private ModSettingsArea modSettingsArea = null!;
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private DifficultyMultiplierDisplay? multiplierDisplay;
protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; }
2022-04-04 14:45:44 +08:00
2022-07-01 19:43:12 +08:00
private Sample? columnAppearSample;
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
}
2022-03-27 05:43:17 +08:00
[BackgroundDependencyLoader]
2022-07-01 19:43:12 +08:00
private void load(OsuGameBase game, OsuColour colours, AudioManager audio)
2022-03-27 05:43:17 +08:00
{
Header.Title = ModSelectOverlayStrings.ModSelectTitle;
Header.Description = ModSelectOverlayStrings.ModSelectDescription;
2022-03-27 05:43:17 +08:00
2022-07-01 19:43:12 +08:00
columnAppearSample = audio.Samples.Get(@"SongSelect/mod-column-pop-in");
AddRange(new Drawable[]
2022-03-27 05:43:17 +08:00
{
new ClickToReturnContainer
{
RelativeSizeAxes = Axes.Both,
HandleMouse = { BindTarget = customisationVisible },
OnClicked = () => customisationVisible.Value = false
},
modSettingsArea = new ModSettingsArea
2022-03-27 05:43:17 +08:00
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0
}
});
MainAreaContent.AddRange(new Drawable[]
2022-03-27 05:43:17 +08:00
{
new OsuContextMenuContainer
{
2022-03-27 05:43:17 +08:00
RelativeSizeAxes = Axes.Both,
Child = new PopoverContainer
2022-03-27 05:43:17 +08:00
{
Padding = new MarginPadding
{
Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
2022-03-27 05:43:17 +08:00
{
columnScroll = new ColumnScrollContainer
2022-03-27 05:43:17 +08:00
{
RelativeSizeAxes = Axes.Both,
Masking = false,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = columnFlow = new ColumnFlowContainer
2022-03-27 05:43:17 +08:00
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Shear = new Vector2(SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 },
Padding = new MarginPadding { Bottom = 10 },
ChildrenEnumerable = createColumns()
}
}
}
2022-03-27 05:43:17 +08:00
}
}
});
if (ShowTotalMultiplier)
{
MainAreaContent.Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.X,
Height = ModsEffectDisplay.HEIGHT,
Margin = new MarginPadding { Horizontal = 100 },
Child = multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
});
}
FooterContent.Child = footerButtonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding
{
Vertical = PADDING,
Horizontal = 70
},
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH)
2022-05-07 04:30:07 +08:00
{
Text = CommonStrings.Back,
Action = Hide,
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1
})
};
2022-05-12 01:02:45 +08:00
globalAvailableMods.BindTo(game.AvailableMods);
2022-03-27 05:43:17 +08:00
}
private ModSettingChangeTracker? modSettingChangeTracker;
2022-03-27 05:43:17 +08:00
protected override void LoadComplete()
{
// this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly.
globalAvailableMods.BindValueChanged(_ => createLocalMods(), true);
2022-05-12 01:02:45 +08:00
base.LoadComplete();
State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true);
2022-05-10 14:07:08 +08:00
// This is an optimisation to prevent refreshing the available settings controls when it can be
// reasonably assumed that the settings panel is never to be displayed (e.g. FreeModSelectOverlay).
if (AllowCustomisation)
((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
SelectedMods.BindValueChanged(_ =>
2022-03-27 05:43:17 +08:00
{
updateMultiplier();
updateFromExternalSelection();
updateCustomisation();
Fix leak of `ModSettingChangeTracker` instances The `SelectedMods.BindValueChanged()` callback in `ModSelectOverlay` can in some instances run recursively. This is most heavily leaned on in scenarios where `SelectedMods` is updated by an external component. In such cases, the mod select overlay needs to replace the mod instances received externally with mod instances which it owns, so that the changes made on the overlay can propagate outwards. This in particular means that prior to this commit, it was possible to encounter the following scenario: modSettingChangeTracker?.Dispose(); updateFromExternalSelection(); // mutates SelectedMods to perform the replacement // therefore causing a recursive call modSettingChangeTracker?.Dispose(); // inner call continues modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); // outer call continues modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); This leaks one `modSettingChangeTracker` instance from the inner call, which is never disposed. To avoid this, move the disposal to the same side of the recursion that the creation happens on, changing the call pattern to: updateFromExternalSelection(); // mutates SelectedMods to perform the replacement // therefore causing a recursive call modSettingChangeTracker?.Dispose(); // inner call continues modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); modSettingChangeTracker?.Dispose(); // outer call continues modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); which, while slightly wasteful, does not cause any leaks. The solution is definitely suboptimal, but addressing this properly would entail a major rewrite of the mod instance management in the mods overlay, which is probably not the wisest move to make right now.
2023-04-30 23:24:07 +08:00
modSettingChangeTracker?.Dispose();
if (AllowCustomisation)
{
// Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can
// potentially be stale, due to complexities in the way change trackers work.
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateMultiplier();
}
2022-03-27 05:43:17 +08:00
}, true);
2022-03-27 05:43:17 +08:00
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
// Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available.
ScheduleAfterChildren(() =>
{
2022-05-08 18:57:03 +08:00
columnScroll.ScrollTo(200, false);
columnScroll.ScrollToStart();
});
2022-03-27 05:43:17 +08:00
}
/// <summary>
/// Select all visible mods in all columns.
/// </summary>
public void SelectAll()
{
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
column.SelectAll();
}
/// <summary>
/// Deselect all visible mods in all columns.
/// </summary>
public void DeselectAll()
{
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
column.DeselectAll();
}
private IEnumerable<ColumnDimContainer> createColumns()
{
if (ShowPresets)
{
yield return new ColumnDimContainer(new ModPresetColumn
{
Margin = new MarginPadding { Right = 10 }
});
}
yield return createModColumnContent(ModType.DifficultyReduction);
yield return createModColumnContent(ModType.DifficultyIncrease);
yield return createModColumnContent(ModType.Automation);
yield return createModColumnContent(ModType.Conversion);
yield return createModColumnContent(ModType.Fun);
}
private ColumnDimContainer createModColumnContent(ModType modType)
{
var column = CreateModColumn(modType).With(column =>
{
// spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden.
column.Margin = new MarginPadding { Right = 10 };
});
return new ColumnDimContainer(column);
}
2022-05-12 01:02:45 +08:00
private void createLocalMods()
{
var newLocalAvailableMods = new Dictionary<ModType, IReadOnlyList<ModState>>();
2022-05-12 01:02:45 +08:00
foreach (var (modType, mods) in globalAvailableMods.Value)
2022-05-12 01:02:45 +08:00
{
var modStates = mods.SelectMany(ModUtils.FlattenMod)
.Select(mod => new ModState(mod.DeepClone()))
.ToArray();
foreach (var modState in modStates)
modState.Active.BindValueChanged(_ => updateFromInternalSelection());
newLocalAvailableMods[modType] = modStates;
2022-05-12 01:02:45 +08:00
}
AvailableMods.Value = newLocalAvailableMods;
2022-05-12 01:02:45 +08:00
filterMods();
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
column.AvailableMods = AvailableMods.Value.GetValueOrDefault(column.ModType, Array.Empty<ModState>());
2022-05-12 01:02:45 +08:00
}
private void filterMods()
{
foreach (var modState in allAvailableMods)
2022-05-12 01:02:45 +08:00
modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod);
}
2022-03-27 05:43:17 +08:00
private void updateMultiplier()
{
if (multiplierDisplay == null)
return;
2022-03-27 05:43:17 +08:00
double multiplier = 1.0;
foreach (var mod in SelectedMods.Value)
2022-03-27 05:43:17 +08:00
multiplier *= mod.ScoreMultiplier;
multiplierDisplay.Current.Value = multiplier;
}
private void updateCustomisation()
2022-03-27 05:43:17 +08:00
{
if (CustomisationButton == null)
return;
bool anyCustomisableModActive = false;
bool anyModPendingConfiguration = false;
2022-03-27 05:43:17 +08:00
foreach (var modState in allAvailableMods)
2022-03-27 05:43:17 +08:00
{
anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any();
anyModPendingConfiguration |= modState.PendingConfiguration;
modState.PendingConfiguration = false;
2022-03-27 05:43:17 +08:00
}
if (anyCustomisableModActive)
2022-03-27 05:43:17 +08:00
{
customisationVisible.Disabled = false;
if (anyModPendingConfiguration && !customisationVisible.Value)
2022-03-27 05:43:17 +08:00
customisationVisible.Value = true;
}
else
{
if (customisationVisible.Value)
customisationVisible.Value = false;
customisationVisible.Disabled = true;
}
}
private void updateCustomisationVisualState()
{
const double transition_duration = 300;
MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic);
2022-03-27 05:43:17 +08:00
foreach (var button in footerButtonFlow)
{
if (button != CustomisationButton)
button.Enabled.Value = !customisationVisible.Value;
}
2022-03-27 05:43:17 +08:00
float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0;
modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
2022-03-27 05:43:17 +08:00
}
/// <summary>
/// This flag helps to determine the source of changes to <see cref="SelectedMods"/>.
/// If the value is false, then <see cref="SelectedMods"/> are changing due to a user selection on the UI.
/// If the value is true, then <see cref="SelectedMods"/> are changing due to an external <see cref="SelectedMods"/> change.
/// </summary>
private bool externalSelectionUpdateInProgress;
private void updateFromExternalSelection()
{
if (externalSelectionUpdateInProgress)
return;
externalSelectionUpdateInProgress = true;
var newSelection = new List<Mod>();
foreach (var modState in allAvailableMods)
{
var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType());
if (matchingSelectedMod != null)
{
modState.Mod.CopyFrom(matchingSelectedMod);
modState.Active.Value = true;
newSelection.Add(modState.Mod);
}
else
{
modState.Mod.ResetSettingsToDefaults();
modState.Active.Value = false;
}
}
SelectedMods.Value = newSelection;
externalSelectionUpdateInProgress = false;
}
private void updateFromInternalSelection()
{
if (externalSelectionUpdateInProgress)
return;
var candidateSelection = allAvailableMods.Where(modState => modState.Active.Value)
.Select(modState => modState.Mod)
.ToArray();
SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection);
}
#region Transition handling
private const float distance = 700;
2022-03-27 05:43:17 +08:00
protected override void PopIn()
{
2022-04-05 17:38:31 +08:00
const double fade_in_duration = 400;
2022-04-04 14:45:44 +08:00
2022-03-27 05:43:17 +08:00
base.PopIn();
2022-04-04 14:45:44 +08:00
multiplierDisplay?
.FadeIn(fade_in_duration, Easing.OutQuint)
.MoveToY(0, fade_in_duration, Easing.OutQuint);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
var column = columnFlow[i].Column;
bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value);
double delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
double duration = allFiltered ? 0 : fade_in_duration;
float startingYPosition = 0;
if (!allFiltered)
startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.TopLevelContent
.MoveToY(startingYPosition)
.Delay(delay)
.MoveToY(0, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
2022-07-01 19:43:12 +08:00
if (allFiltered)
continue;
int columnNumber = nonFilteredColumnCount;
Scheduler.AddDelayed(() =>
{
var channel = columnAppearSample?.GetChannel();
if (channel == null) return;
// Still play sound effects for off-screen columns up to a certain point.
2022-07-01 19:43:12 +08:00
if (columnNumber > 5 && !column.Active.Value) return;
// use X position of the column on screen as a basis for panning the sample
float balance = column.Parent.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X;
2022-07-01 19:43:12 +08:00
// dip frequency and ramp volume of sample over the first 5 displayed columns
float progress = Math.Min(1, columnNumber / 5f);
channel.Frequency.Value = 1.3 - (progress * 0.3) + RNG.NextDouble(0.1);
channel.Volume.Value = Math.Max(progress, 0.2);
channel.Balance.Value = -1 + balance * 2;
channel.Play();
}, delay);
nonFilteredColumnCount += 1;
}
2022-03-27 05:43:17 +08:00
}
protected override void PopOut()
{
2022-04-04 14:45:44 +08:00
const double fade_out_duration = 500;
2022-03-27 05:43:17 +08:00
base.PopOut();
2022-04-04 14:45:44 +08:00
multiplierDisplay?
2022-04-05 17:38:31 +08:00
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
2022-04-05 17:25:27 +08:00
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
var column = columnFlow[i].Column;
bool allFiltered = false;
if (column is ModColumn modColumn)
{
allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value);
modColumn.FlushPendingSelections();
}
double duration = allFiltered ? 0 : fade_out_duration;
float newYPosition = 0;
if (!allFiltered)
newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.TopLevelContent
.MoveToY(newYPosition, duration, Easing.OutQuint)
.FadeOut(duration, Easing.OutQuint);
if (!allFiltered)
nonFilteredColumnCount += 1;
}
2022-03-27 05:43:17 +08:00
}
#endregion
#region Input handling
2022-03-27 05:43:17 +08:00
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.Back:
// Pressing the back binding should only go back one step at a time.
hideOverlay(false);
return true;
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection:
case GlobalAction.Select:
{
// Pressing toggle or select should completely hide the overlay in one shot.
hideOverlay(true);
return true;
}
}
return base.OnPressed(e);
void hideOverlay(bool immediate)
{
if (customisationVisible.Value)
{
Debug.Assert(CustomisationButton != null);
CustomisationButton.TriggerClick();
if (!immediate)
return;
}
BackButton.TriggerClick();
}
}
#endregion
#region Sample playback control
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool(true);
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
#endregion
2022-05-08 01:03:28 +08:00
/// <summary>
/// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
/// </summary>
[Cached]
internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{
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;
}
}
}
2022-05-08 01:03:28 +08:00
/// <summary>
/// Manages layout of mod columns.
2022-05-08 01:03:28 +08:00
/// </summary>
internal partial class ColumnFlowContainer : FillFlowContainer<ColumnDimContainer>
2022-03-27 05:43:17 +08:00
{
public IEnumerable<ModSelectColumn> Columns => Children.Select(dimWrapper => dimWrapper.Column);
public override void Add(ColumnDimContainer dimContainer)
2022-03-27 05:43:17 +08:00
{
base.Add(dimContainer);
2022-03-27 05:43:17 +08:00
Debug.Assert(dimContainer != null);
dimContainer.Column.Shear = Vector2.Zero;
2022-03-27 05:43:17 +08:00
}
}
2022-05-08 01:03:28 +08:00
/// <summary>
/// Encapsulates a column and provides dim and input blocking based on an externally managed "active" state.
/// </summary>
internal partial class ColumnDimContainer : Container
{
public ModSelectColumn Column { get; }
2022-05-08 01:03:28 +08:00
/// <summary>
/// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen.
/// </summary>
public readonly Bindable<bool> Active = new BindableBool();
2022-05-08 01:03:28 +08:00
/// <summary>
/// Invoked when the column is clicked while not active, requesting a scroll to be performed to bring it on-screen.
/// </summary>
public Action<ColumnDimContainer>? RequestScroll { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
public ColumnDimContainer(ModSelectColumn column)
{
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Child = Column = column;
column.Active.BindTo(Active);
}
[BackgroundDependencyLoader]
private void load(ColumnScrollContainer columnScroll)
{
RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140);
}
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ => updateState(), true);
FinishTransforms();
}
protected override bool RequiresChildrenUpdate
{
get
{
bool result = base.RequiresChildrenUpdate;
if (Column is ModColumn modColumn)
result |= !modColumn.ItemsLoaded || modColumn.SelectionAnimationRunning;
return result;
}
}
private void updateState()
{
Colour4 targetColour;
if (Column.Active.Value)
targetColour = Colour4.White;
else
targetColour = IsHovered ? colours.GrayC : colours.Gray8;
2022-04-28 13:59:39 +08:00
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);
updateState();
return Active.Value;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
}
2022-05-08 01:03:28 +08:00
/// <summary>
/// A container which blocks and handles input, managing the "return from customisation" state change.
/// </summary>
2022-03-27 05:43:17 +08:00
private partial class ClickToReturnContainer : Container
{
public BindableBool HandleMouse { get; } = new BindableBool();
public Action? OnClicked { get; set; }
2022-03-27 05:43:17 +08:00
public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value;
2022-03-27 05:43:17 +08:00
protected override bool Handle(UIEvent e)
{
if (!HandleMouse.Value)
return base.Handle(e);
switch (e)
{
2022-06-24 20:25:23 +08:00
case ClickEvent:
2022-03-27 05:43:17 +08:00
OnClicked?.Invoke();
return true;
2022-06-24 20:25:23 +08:00
case MouseEvent:
2022-03-27 05:43:17 +08:00
return true;
}
return base.Handle(e);
}
}
}
}