From 2e89b4cee4b7c355fcbe63d54e994ea6f92d586d Mon Sep 17 00:00:00 2001 From: SnipUndercover Date: Tue, 6 Feb 2024 20:46:06 +0100 Subject: [PATCH] Add `TextMenuExt.FloatSlider` --- Celeste.Mod.mm/Mod/UI/TextMenuExt.cs | 212 +++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs index 9bd222bf2..0d1873b71 100644 --- a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs +++ b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs @@ -1,5 +1,6 @@ using Celeste.Mod; using Celeste.Mod.Core; +using Celeste.Mod.UI; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Monocle; @@ -426,6 +427,217 @@ public override void Render(Vector2 position, bool highlighted) { } } + + /// + /// A Slider optimized for float ranges.

+ /// Inherits directly from + ///
+ public class FloatSlider : TextMenu.Item { + public string Label; + + private float _Value; + public float Value { + get => MathF.Round(_Value, Precision); + set => _Value = Calc.Clamp(value, Min, Max); + } + private float _PreviousValue; + public float PreviousValue { + get => MathF.Round(_PreviousValue, Precision); + set => _PreviousValue = value; + } + + public Action OnValueChange; + private readonly Func ValueFormat; + + // let's be reasonable here + private const int MaxPrecision = 5; + private const int MinPrecision = 1; + + private readonly int Precision; + + private readonly float Min; + private readonly float Max; + + private readonly int MaxScrollOrderOfMagnitude; + + // increase order of magnitude after a second, every 2 seconds of holding the direction + // but don't go too high, we don't want to overshoot + private int ScrollOrderOfMagnitude + => (int) Calc.Clamp(MathF.Truncate((ScrollTimer + 1) / 2), 0f, MaxScrollOrderOfMagnitude); + + private float Sine; + private int LastScrollDirection; + private float ScrollTimer; + + /// + /// Creates a new . + /// + /// Slider label + /// Minimum allowed value + /// Maximum allowed value + /// Initial value, restricted between and + /// Slider precision; between 1 and 5 digits of precision + /// Value formatter, defaults to + public FloatSlider(string label, float min, float max, float value = 0, int precision = 1, Func valueFormat = null) { + Label = label; + Selectable = true; + + Precision = Calc.Clamp(precision, MinPrecision, MaxPrecision); + + Min = min; + Max = max; + + // find the max order of magnitude to scroll by + // floor(log_10(x))+1 returns the number of digits in x (before the decimal point) + // then we add the digits after the decimal point (precision) + MaxScrollOrderOfMagnitude = (int) MathF.Floor(MathF.Log10(max - min)) + 1 + precision; + + ValueFormat = valueFormat ?? (value => value.ToString()); + + Value = value; // the accessor will clamp our value + } + + /// + public FloatSlider Change(Action action) { + OnValueChange = action; + return this; + } + + public override void Added() { + Container.InnerContent = TextMenu.InnerContentMode.TwoColumn; + } + + public override void LeftPressed() { + if (Input.MenuLeft.Repeating) + ScrollTimer += Engine.RawDeltaTime * 8; + else + ScrollTimer = 0; + + if (Value > Min) { + Audio.Play(SFX.ui_main_button_toggle_off); + + PreviousValue = Value; + Value -= MathF.Pow(10, ScrollOrderOfMagnitude - Precision); + Value = Math.Max(Min, Value); // ensure we stay within bounds + LastScrollDirection = -1; + + ValueWiggler.Start(); + OnValueChange?.Invoke(Value); + } + } + + public override void RightPressed() { + if (Input.MenuRight.Repeating) + ScrollTimer += Engine.RawDeltaTime * 8; + else + ScrollTimer = 0; + + if (Value < Max) { + Audio.Play(SFX.ui_main_button_toggle_on); + + PreviousValue = Value; + Value += MathF.Pow(10, ScrollOrderOfMagnitude - Precision); + Value = Math.Min(Max, Value); // ensure we stay within bounds + LastScrollDirection = 1; + + ValueWiggler.Start(); + OnValueChange?.Invoke(Value); + } + } + + public override void ConfirmPressed() { + if (Engine.Scene is not Overworld overworld) { + // can't enter OUIs in-game! + Audio.Play(SFX.ui_main_button_invalid); + return; + } + overworld.Goto().Init( + Value, + (value) => { + PreviousValue = Value; + Value = Calc.Clamp(value, Min, Max); + LastScrollDirection = 0; + + OnValueChange?.Invoke(value); + }, + maxValueLength: (int) MathF.Floor(MathF.Log10(Max - Min)) + 1 + Precision, + allowDecimals: true, + allowNegatives: Min < 0 + ); + } + + public override void Update() { + Sine += Engine.RawDeltaTime; + } + + public override float LeftWidth() { + return ActiveFont.Measure(Label).X + 32f; + } + + public override float RightWidth() { + // Measure value in case it is externally set ouside the bounds + float width = Calc.Max( + 0f, + ActiveFont.Measure(ValueFormat.Invoke(Max)).X, + ActiveFont.Measure(ValueFormat.Invoke(Min)).X, + ActiveFont.Measure(ValueFormat.Invoke(Value)).X + ); + return width + 120f; + } + + public override float Height() { + return ActiveFont.LineHeight; + } + + public override void Render(Vector2 position, bool highlighted) { + float alpha = Container.Alpha; + Color strokeColor = Color.Black * (alpha * alpha * alpha); + Color color = Disabled + ? Color.DarkSlateGray + : ((highlighted ? Container.HighlightColor : Color.White) * alpha); + Color disabledColor = Color.DarkSlateGray * alpha; + + ActiveFont.DrawOutline( + Label, + position, new Vector2(0f, 0.5f), Vector2.One, + color, + 2f, strokeColor + ); + + // someone messed up the ranges! we're outta here + if (Max - Min <= 0) + return; + + float rWidth = RightWidth(); + ActiveFont.DrawOutline( + ValueFormat.Invoke(Value), + position + new Vector2(Container.Width - rWidth * 0.5f + LastScrollDirection * ValueWiggler.Value * 8f, 0f), new Vector2(0.5f, 0.5f), Vector2.One * 0.8f, + color, + 2f, strokeColor + ); + + Vector2 sineOffset = Vector2.UnitX * (highlighted ? (MathF.Sin(Sine * 4f) * 4f) : 0f); + + Vector2 adjustmentIndicatorPosition = + position + + new Vector2(Container.Width - rWidth + 40f + ((LastScrollDirection < 0) ? (-ValueWiggler.Value * 8f) : 0f), 0f) + - (Value > Min ? sineOffset : Vector2.Zero); + + ActiveFont.DrawOutline( + "<", + adjustmentIndicatorPosition, new Vector2(0.5f, 0.5f), Vector2.One, + Value > Min ? color : disabledColor, + 2f, strokeColor + ); + + adjustmentIndicatorPosition = + position + + new Vector2(Container.Width - 40f + ((LastScrollDirection > 0) ? (ValueWiggler.Value * 8f) : 0f), 0f) + + (Value < Max ? sineOffset : Vector2.Zero); + ActiveFont.DrawOutline(">", adjustmentIndicatorPosition, new Vector2(0.5f, 0.5f), Vector2.One, Value < Max ? color : disabledColor, 2f, strokeColor); + } + } + /// /// that acts as a Submenu for other Items. ///