From 18fd4758d736b2a14aead9a11ad9a1256df186c7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 12 Jan 2026 23:28:10 -0500 Subject: [PATCH 1/3] Update form button UI/UX and support text wrapping --- .../Graphics/UserInterfaceV2/FormButton.cs | 176 ++++++++++++++---- 1 file changed, 137 insertions(+), 39 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index 85198191b8..d10134911f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -28,62 +30,137 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public LocalisableString Caption { get; init; } + /// + /// Sets text inside the button. + /// public LocalisableString ButtonText { get; init; } - public Action? Action { get; init; } + /// + /// Sets a custom button icon. Not shown when is set. + /// + public IconUsage ButtonIcon { get; init; } = FontAwesome.Solid.ChevronRight; + + private Color4? backgroundColour; + + /// + /// Sets a custom background colour for the button. + /// + public Color4? BackgroundColour + { + get => backgroundColour; + set + { + backgroundColour = value; + + if (IsLoaded && value != null) + button.BackgroundColour = value.Value; + } + } + + /// + /// The action to invoke when the button is clicked. + /// + public Action? Action { get; set; } + + /// + /// Whether the button is enabled. + /// + public readonly BindableBool Enabled = new BindableBool(true); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private Container content = null!; + private Box background = null!; + private OsuTextFlowContainer text = null!; + private Button button = null!; + [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.X; - Height = 50; + AutoSizeAxes = Axes.Y; - Masking = true; - CornerRadius = 5; - CornerExponent = 2.5f; - - InternalChildren = new Drawable[] + InternalChild = content = new Container { - new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 5, + CornerExponent = 2.5f, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + background = new Box { - Left = 9, - Right = 5, - Vertical = 5, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + new TrianglesV2 { - new OsuTextFlowContainer + SpawnRatio = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background5), + }, + new HoverClickSounds(HoverSampleSet.Button) + { + Enabled = { BindTarget = Enabled }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = 0.45f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = Caption, + Left = 9, + Right = 5, + Vertical = 5, }, - new Button + Children = new Drawable[] { - Action = Action, - Text = ButtonText, - RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X, - Width = ButtonText == default ? 90 : 0.45f, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + text = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Caption, + }, + button = new Button + { + Action = () => Action?.Invoke(), + Text = ButtonText, + Icon = ButtonIcon, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Enabled = { BindTarget = Enabled }, + } + }, }, - }, + } }; + + if (ButtonText == default) + { + text.Padding = new MarginPadding { Right = 100 }; + button.Width = 90; + } + else + { + text.Width = 0.55f; + text.Padding = new MarginPadding { Right = 10 }; + button.RelativeSizeAxes = Axes.X; + button.Width = 0.45f; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (BackgroundColour != null) + button.BackgroundColour = BackgroundColour.Value; + + Enabled.BindValueChanged(_ => updateState(), true); } protected override bool OnHover(HoverEvent e) @@ -98,12 +175,31 @@ namespace osu.Game.Graphics.UserInterfaceV2 updateState(); } + protected override bool OnClick(ClickEvent e) + { + if (Enabled.Value) + { + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + button.TriggerClick(); + } + + return true; + } + private void updateState() { - BorderThickness = IsHovered ? 2 : 0; + text.Colour = !Enabled.Value ? colourProvider.Background1 : colourProvider.Content1; - if (IsHovered) - BorderColour = colourProvider.Light4; + background.FadeColour(IsHovered + ? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4) + : colourProvider.Background5, 200, Easing.OutQuint); + + content.BorderThickness = IsHovered ? 2 : 0; + + if (!Enabled.Value) + content.BorderColour = BackgroundColour != null ? Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1) : colourProvider.Dark1; + else + content.BorderColour = BackgroundColour ?? colourProvider.Light4; } public partial class Button : OsuButton @@ -125,6 +221,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } + public IconUsage Icon { get; init; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider overlayColourProvider) { @@ -135,7 +233,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Add(new SpriteIcon { - Icon = FontAwesome.Solid.ChevronRight, + Icon = Icon, Size = new Vector2(16), Shadow = true, Anchor = Anchor.Centre, From df1304af9e1042823352f9e6b9ce325601a311e2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 12 Jan 2026 23:28:14 -0500 Subject: [PATCH 2/3] Add visual test --- .../UserInterface/TestSceneFormButton.cs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs new file mode 100644 index 0000000000..a22607e781 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormButton.cs @@ -0,0 +1,139 @@ +// 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.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormButton : ThemeComparisonTestScene + { + public TestSceneFormButton() + : base(false) + { + } + + protected override Drawable CreateContent() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new BackgroundBox + { + RelativeSizeAxes = Axes.Both, + }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 400, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FormButton + { + Caption = "Button with default style", + Action = () => { }, + }, + new FormButton + { + Caption = "Button with default style", + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Button with custom style", + BackgroundColour = new OsuColour().DangerousButtonColour, + ButtonIcon = FontAwesome.Solid.Hamburger, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with custom style", + BackgroundColour = new OsuColour().DangerousButtonColour, + ButtonIcon = FontAwesome.Solid.Hamburger, + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", + BackgroundColour = new OsuColour().Blue3, + ButtonIcon = FontAwesome.Solid.Book, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().DangerousButtonColour, + Action = () => { }, + }, + new FormButton + { + Caption = "Button with text inside", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().DangerousButtonColour, + Enabled = { Value = false }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().Blue3, + Action = () => { }, + }, + new FormButton + { + Caption = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + ButtonText = "Text in button", + BackgroundColour = new OsuColour().Blue3, + Enabled = { Value = false }, + }, + }, + }, + }, + } + } + }; + + private partial class BackgroundBox : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background4; + } + } + } +} From 3f4d1b798e0ca5919d2078ea78d6c6ce74165132 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 14 Jan 2026 01:33:42 -0500 Subject: [PATCH 3/3] Update button background colour in update function --- .../Graphics/UserInterfaceV2/FormButton.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index d10134911f..7b95a7e976 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public IconUsage ButtonIcon { get; init; } = FontAwesome.Solid.ChevronRight; - private Color4? backgroundColour; + private readonly Color4? backgroundColour; /// /// Sets a custom background colour for the button. @@ -48,12 +48,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public Color4? BackgroundColour { get => backgroundColour; - set + init { backgroundColour = value; - if (IsLoaded && value != null) - button.BackgroundColour = value.Value; + if (IsLoaded) + updateState(); } } @@ -156,10 +156,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void LoadComplete() { base.LoadComplete(); - - if (BackgroundColour != null) - button.BackgroundColour = BackgroundColour.Value; - Enabled.BindValueChanged(_ => updateState(), true); } @@ -188,7 +184,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateState() { - text.Colour = !Enabled.Value ? colourProvider.Background1 : colourProvider.Content1; + text.Colour = Enabled.Value ? colourProvider.Content1 : colourProvider.Background1; background.FadeColour(IsHovered ? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4) @@ -196,10 +192,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 content.BorderThickness = IsHovered ? 2 : 0; - if (!Enabled.Value) - content.BorderColour = BackgroundColour != null ? Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1) : colourProvider.Dark1; + if (BackgroundColour != null) + { + button.BackgroundColour = BackgroundColour.Value; + content.BorderColour = Enabled.Value ? BackgroundColour.Value : Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1); + } else - content.BorderColour = BackgroundColour ?? colourProvider.Light4; + content.BorderColour = Enabled.Value ? colourProvider.Light4 : colourProvider.Dark1; } public partial class Button : OsuButton