// 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.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osuTK; using osuTK.Input; #nullable enable namespace osu.Game.Overlays.Mods { public class ModPanel : OsuClickableContainer { public Mod Mod { get; } public BindableBool Active { get; } = new BindableBool(); public BindableBool Filtered { get; } = new BindableBool(); protected readonly Box Background; protected readonly Container SwitchContainer; protected readonly Container MainContentContainer; protected readonly Box TextBackground; protected readonly FillFlowContainer TextFlow; [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } = null!; protected const double TRANSITION_DURATION = 150; public const float CORNER_RADIUS = 7; protected const float HEIGHT = 42; protected const float IDLE_SWITCH_WIDTH = 54; protected const float EXPANDED_SWITCH_WIDTH = 70; private Colour4 activeColour; private Sample? sampleOff; private Sample? sampleOn; public ModPanel(Mod mod) { Mod = mod; RelativeSizeAxes = Axes.X; Height = 42; // all below properties are applied to `Content` rather than the `ModPanel` in its entirety // to allow external components to set these properties on the panel without affecting // its "internal" appearance. Content.Masking = true; Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; Content.Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); Children = new Drawable[] { Background = new Box { RelativeSizeAxes = Axes.Both }, SwitchContainer = new Container { RelativeSizeAxes = Axes.Y, Child = new ModSwitchSmall(mod) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) } }, MainContentContainer = new Container { RelativeSizeAxes = Axes.Both, Child = new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = CORNER_RADIUS, Children = new Drawable[] { TextBackground = new Box { RelativeSizeAxes = Axes.Both }, TextFlow = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 17.5f, Vertical = 4 }, Direction = FillDirection.Vertical, Children = new[] { new OsuSpriteText { Text = mod.Name, Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Margin = new MarginPadding { Left = -18 * ShearedOverlayContainer.SHEAR } }, new OsuSpriteText { Text = mod.Description, Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, Truncate = true, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) } } } } } } }; Action = Active.Toggle; } [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); activeColour = colours.ForModType(Mod.Type); } protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); protected override void LoadComplete() { base.LoadComplete(); Active.BindValueChanged(_ => { playStateChangeSamples(); UpdateState(); }); Filtered.BindValueChanged(_ => updateFilterState(), true); UpdateState(); FinishTransforms(true); } private void playStateChangeSamples() { if (Active.Value) sampleOn?.Play(); else sampleOff?.Play(); } protected override bool OnHover(HoverEvent e) { UpdateState(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { UpdateState(); base.OnHoverLost(e); } private bool mouseDown; protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == MouseButton.Left) mouseDown = true; UpdateState(); return false; } protected override void OnMouseUp(MouseUpEvent e) { mouseDown = false; UpdateState(); base.OnMouseUp(e); } protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : (Colour4)ColourProvider.Background3; protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : (Colour4)ColourProvider.Background2; protected virtual Colour4 TextColour => Active.Value ? (Colour4)ColourProvider.Background6 : Colour4.White; protected virtual void UpdateState() { float targetWidth = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; double transitionDuration = TRANSITION_DURATION; Colour4 backgroundColour = BackgroundColour; Colour4 foregroundColour = ForegroundColour; Colour4 textColour = TextColour; // Hover affects colour of button background if (IsHovered) { backgroundColour = backgroundColour.Lighten(0.1f); foregroundColour = foregroundColour.Lighten(0.1f); } // Mouse down adds a halfway tween of the movement if (mouseDown) { targetWidth = (float)Interpolation.Lerp(IDLE_SWITCH_WIDTH, EXPANDED_SWITCH_WIDTH, 0.5f); transitionDuration *= 4; } Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint); Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint); SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint); MainContentContainer.TransformTo(nameof(Padding), new MarginPadding { Left = targetWidth, Right = CORNER_RADIUS }, transitionDuration, Easing.OutQuint); TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } #region Filtering support public void ApplyFilter(Func? filter) { Filtered.Value = filter != null && !filter.Invoke(Mod); } private void updateFilterState() { this.FadeTo(Filtered.Value ? 0 : 1); } #endregion } }