// 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.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { public partial class ShearedRangeSlider : CompositeDrawable { private readonly LocalisableString label; private readonly BindableNumberWithCurrent lowerBound = new BindableNumberWithCurrent(); /// /// The lower limiting value. /// public Bindable LowerBound { get => lowerBound.Current; set => lowerBound.Current = value; } private readonly BindableNumberWithCurrent upperBound = new BindableNumberWithCurrent(); /// /// The upper limiting value. /// public Bindable UpperBound { get => upperBound.Current; set => upperBound.Current = value; } public float NubWidth { get; init; } /// /// Minimum difference between the lower bound and higher bound /// public float MinRange { set => minRange = value; } /// /// Lower bound display for when it is set to its default value, or null to display the value directly. /// public LocalisableString? DefaultStringLowerBound { get; init; } /// /// Upper bound display for when it is set to its default value, or null to display the value directly. /// public LocalisableString? DefaultStringUpperBound { get; init; } private float minRange = 0.1f; protected Container SliderContainer { get; private set; } = null!; protected BoundSliderBar LowerBoundSlider { get; private set; } = null!; protected BoundSliderBar UpperBoundSlider { get; private set; } = null!; protected Vector2 ScreenSpaceHalfwayPoint { get { var lowerSS = LowerBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft; var upperSS = UpperBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft; return lowerSS + (upperSS - lowerSS) / 2; } } public ShearedRangeSlider(LocalisableString label) { this.label = label; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { Height = ShearedNub.HEIGHT; InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), }, Content = new[] { new[] { new Container { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, Masking = true, CornerRadius = 5f, Shear = OsuGame.SHEAR, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, }, new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = label, Shear = -OsuGame.SHEAR, Margin = new MarginPadding { Horizontal = 12, Vertical = 5 }, Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, }, }, SliderContainer = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = -10 }, Children = new[] { UpperBoundSlider = CreateBoundSlider(true).With(d => { d.KeyboardStep = 0.1f; d.RelativeSizeAxes = Axes.X; d.DefaultString = DefaultStringUpperBound; d.NubWidth = NubWidth; d.Current = upperBound; }), LowerBoundSlider = CreateBoundSlider(false).With(d => { d.KeyboardStep = 0.1f; d.RelativeSizeAxes = Axes.X; d.DefaultString = DefaultStringLowerBound; d.NubWidth = NubWidth; d.Current = lowerBound; }), UpperBoundSlider.Nub.CreateProxy(), LowerBoundSlider.Nub.CreateProxy(), }, }, }, }, }; } protected override void LoadComplete() { base.LoadComplete(); LowerBoundSlider.Current.ValueChanged += min => UpperBoundSlider.Current.Value = Math.Max(min.NewValue + minRange, UpperBoundSlider.Current.Value); UpperBoundSlider.Current.ValueChanged += max => LowerBoundSlider.Current.Value = Math.Min(max.NewValue - minRange, LowerBoundSlider.Current.Value); } protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(this, isUpper); protected partial class BoundSliderBar : ShearedSliderBar { private readonly ShearedRangeSlider rangeSlider; private readonly bool isUpper; public new float NormalizedValue => base.NormalizedValue; public new ShearedNub Nub => base.Nub; public LocalisableString? DefaultString; public float NubWidth { get; set; } = ShearedNub.HEIGHT; public override LocalisableString TooltipText { get { if (Current.IsDefault) return string.Empty; return Current.Value.ToLocalisableString(@"N1"); } } protected OsuSpriteText NubText { get; private set; } = null!; public override bool AcceptsFocus => false; public BoundSliderBar(ShearedRangeSlider rangeSlider, bool isUpper) { this.rangeSlider = rangeSlider; this.isUpper = isUpper; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { Nub.Width = NubWidth; RangePadding = Nub.Width / 2; Nub.Add(NubText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, X = -3, UseFullGlyphHeight = false, Colour = OsuColour.ForegroundTextColourFor(colourProvider.Light1), Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }); AccentColour = colourProvider.Highlight1.Darken(0.1f); Nub.AccentColour = colourProvider.Highlight1; Nub.GlowingAccentColour = colourProvider.Highlight1; Nub.GlowColour = colourProvider.Highlight1; } protected override void LoadComplete() { base.LoadComplete(); if (!isUpper) { AccentColour = BackgroundColour; BackgroundColour = Color4.Transparent; } Current.BindValueChanged(current => UpdateDisplay(current.NewValue), true); FinishTransforms(true); } protected virtual void UpdateDisplay(double value) { if (Current.IsDefault && DefaultString != null) NubText.Text = DefaultString.Value; else NubText.Text = value.ToLocalisableString(@"N1"); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { if (isUpper) return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X; return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X; } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); if (isUpper) { // Only draw left box where required to avoid masking bleed issues. LeftBox.X = ToParentSpace(ToLocalSpace(rangeSlider.LowerBoundSlider.Nub.ScreenSpaceDrawQuad.Centre)).X; LeftBox.Size -= new Vector2(LeftBox.X, 0); } } protected override bool OnHover(HoverEvent e) { base.OnHover(e); return true; // Make sure only one nub shows hover effect at once. } } } }