From 78043fa78237fb05ee40c85ce5b9bdc9c60fad80 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 18 Dec 2025 04:09:02 -0500 Subject: [PATCH] Implement new settings item component --- osu.Game/Overlays/Settings/SettingsItemV2.cs | 176 ++++++++++++++++++ osu.Game/Overlays/Settings/SettingsNote.cs | 117 ++++++++++++ .../Settings/SettingsRevertToDefaultButton.cs | 95 ++++++++++ osu.Game/Overlays/SettingsPanel.cs | 3 + 4 files changed, 391 insertions(+) create mode 100644 osu.Game/Overlays/Settings/SettingsItemV2.cs create mode 100644 osu.Game/Overlays/Settings/SettingsNote.cs create mode 100644 osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs diff --git a/osu.Game/Overlays/Settings/SettingsItemV2.cs b/osu.Game/Overlays/Settings/SettingsItemV2.cs new file mode 100644 index 0000000000..21df39df38 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsItemV2.cs @@ -0,0 +1,176 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Overlays.Settings +{ + public sealed partial class SettingsItemV2 : CompositeDrawable, ISettingsItem, IConditionalFilterable + { + private readonly IFormControl control; + private readonly SettingsRevertToDefaultButton revertButton; + + private readonly BindableBool controlDefault = new BindableBool(true); + private readonly BindableBool controlEnabled = new BindableBool(true); + + /// + /// Whether a revert-to-default button should be displayed. + /// + public bool ShowDefaultRevertButton { get; init; } = true; + + /// + /// A note to display underneath the setting. + /// + public readonly Bindable Note = new Bindable(); + + public SettingsItemV2(IFormControl control) + { + this.control = control; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS_RIGHT }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + revertButton = new SettingsRevertToDefaultButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = ApplyDefault, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = (Drawable)control, + } + } + }, + new SettingsNote + { + RelativeSizeAxes = Axes.X, + Current = { BindTarget = Note }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlDefault.Value = control.IsValueDefault; + controlEnabled.Value = !control.IsDisabled; + + controlDefault.BindValueChanged(_ => updateDefaultState()); + controlEnabled.BindValueChanged(_ => updateDefaultState(), true); + FinishTransforms(true); + } + + private void updateDefaultState() + { + bool showRevertButton = !controlDefault.Value && controlEnabled.Value && ShowDefaultRevertButton; + + if (showRevertButton) + revertButton.Show(); + else + revertButton.Hide(); + } + + protected override void Update() + { + base.Update(); + controlDefault.Value = control.IsValueDefault; + controlEnabled.Value = !control.IsDisabled; + } + + #region ISettingsItem + + public bool HasClassicDefault { get; init; } + + public void ApplyClassicDefault() + { + // will be removed soon. + throw new NotSupportedException(); + } + + public void ApplyDefault() + { + if (!control.IsDisabled) + control.SetValueDefault(); + } + + public event Action SettingChanged + { + add => control.ValueChanged += value; + remove => control.ValueChanged -= value; + } + + #endregion + + #region Filtering + + public const string CLASSIC_DEFAULT_SEARCH_TERM = @"has-classic-default"; + + public IEnumerable Keywords { get; init; } = Enumerable.Empty(); + + public IEnumerable FilterTerms + { + get + { + var filterTerms = new List(Keywords.Select(k => (LocalisableString)k)); + filterTerms.AddRange(control.FilterTerms); + + if (HasClassicDefault) + filterTerms.Add(CLASSIC_DEFAULT_SEARCH_TERM); + + return filterTerms; + } + } + + private bool matchingFilter = true; + + public bool MatchingFilter + { + get => matchingFilter; + set + { + bool wasPresent = IsPresent; + + matchingFilter = value; + + if (IsPresent != wasPresent) + Invalidate(Invalidation.Presence); + } + } + + public override bool IsPresent => base.IsPresent && MatchingFilter; + + public bool FilteringActive { get; set; } + + public BindableBool CanBeShown { get; } = new BindableBool(true); + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + #endregion + } +} diff --git a/osu.Game/Overlays/Settings/SettingsNote.cs b/osu.Game/Overlays/Settings/SettingsNote.cs new file mode 100644 index 0000000000..5ddbfa9fcd --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsNote.cs @@ -0,0 +1,117 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public sealed partial class SettingsNote : CompositeDrawable + { + public readonly Bindable Current = new Bindable(); + + private Box background = null!; + private OsuTextFlowContainer text = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeDuration = 300; + AutoSizeEasing = Easing.OutQuint; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 5, Bottom = 5 }, + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = 5, + CornerExponent = 2.5f, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuTextFlowContainer(s => s.Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold)) + { + Padding = new MarginPadding(8), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(_ => updateDisplay(), true); + FinishTransforms(true); + } + + private void updateDisplay() + { + ClearTransforms(); + + if (Current.Value == null) + { + AutoSizeAxes = Axes.None; + this.ResizeHeightTo(0, 300, Easing.OutQuint); + this.FadeOut(250, Easing.OutQuint); + return; + } + + AutoSizeAxes = Axes.Y; + this.FadeIn(250, Easing.OutQuint); + + switch (Current.Value.Type) + { + case Type.Informational: + background.Colour = colourProvider.Dark2; + text.Colour = colourProvider.Content2; + break; + + case Type.Warning: + background.Colour = colours.Orange1; + text.Colour = colourProvider.Background5; + break; + + case Type.Critical: + background.Colour = colours.Red1; + text.Colour = colourProvider.Background5; + break; + } + + text.Text = Current.Value.Text; + } + + public record Data(LocalisableString Text, Type Type); + + public enum Type + { + Informational, + Warning, + Critical, + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs b/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs new file mode 100644 index 0000000000..305a30a2f5 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsRevertToDefaultButton.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Overlays.Settings +{ + public partial class SettingsRevertToDefaultButton : OsuClickableContainer + { + public const float WIDTH = 32; + + public float IconSize { get; init; } = 14; + + private Box background = null!; + private SpriteIcon spriteIcon = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + // this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button. + public override bool AcceptsFocus => true; + + public SettingsRevertToDefaultButton() + { + Size = new Vector2(WIDTH, 50); + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + spriteIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Light1, + Icon = OsuIcon.Undo, + Margin = new MarginPadding { Left = 12, Right = 5 }, + Size = new Vector2(IconSize), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Enabled.BindValueChanged(_ => updateDisplay(), true); + } + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + public override void Show() + { + this.FadeIn().MoveToX(WIDTH - 10, 300, Easing.OutQuint); + } + + public override void Hide() + { + this.MoveToX(0, 300, Easing.OutQuint).Then().FadeOut(); + } + + private void updateDisplay() + { + spriteIcon.FadeColour(IsHovered ? colourProvider.Content2 : colourProvider.Light1, 300, Easing.OutQuint); + background.FadeColour(IsHovered ? colourProvider.Background2 : colourProvider.Background3, 300, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 9b268c573f..8fd12e1e13 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -31,6 +31,9 @@ namespace osu.Game.Overlays { public const float CONTENT_MARGINS = 20; + // extra margin to give room to the revert-to-default button in settings controls. + public const float CONTENT_MARGINS_RIGHT = 30; + public const float TRANSITION_LENGTH = 600; private const float sidebar_width = SettingsSidebar.EXPANDED_WIDTH;