From b7a56c8a4587fd197624c27ef8fba367edc8bebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Sep 2024 13:57:53 +0200 Subject: [PATCH 1/2] Implement "form" text box control --- .../UserInterface/TestSceneFormControls.cs | 61 +++++ .../UserInterface/ThemeComparisonTestScene.cs | 2 +- .../UserInterfaceV2/FormFieldCaption.cs | 68 +++++ .../Graphics/UserInterfaceV2/FormNumberBox.cs | 23 ++ .../Graphics/UserInterfaceV2/FormTextBox.cs | 238 ++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs new file mode 100644 index 0000000000..f5bc40c869 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -0,0 +1,61 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormControls : ThemeComparisonTestScene + { + public TestSceneFormControls() + : base(false) + { + } + + protected override Drawable CreateContent() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, + }, + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + }, + }, + }, + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 4700ef72d9..44133d89f8 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 + Colour = colourProvider.Background3 }, CreateContent() } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs new file mode 100644 index 0000000000..75c27618e9 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs @@ -0,0 +1,68 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormFieldCaption : CompositeDrawable, IHasTooltip + { + private LocalisableString caption; + + public LocalisableString Caption + { + get => caption; + set + { + caption = value; + + if (captionText.IsNotNull()) + captionText.Text = value; + } + } + + private OsuSpriteText captionText = null!; + + public LocalisableString TooltipText { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + captionText = new OsuSpriteText + { + Text = caption, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = TooltipText == default ? 0 : 1, + Size = new Vector2(10), + Icon = FontAwesome.Solid.QuestionCircle, + Margin = new MarginPadding { Top = 1, }, + } + }, + }; + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs new file mode 100644 index 0000000000..66f1a45210 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormNumberBox : FormTextBox + { + public bool AllowDecimals { get; init; } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox + { + AllowDecimals = AllowDecimals, + }; + + internal partial class InnerNumberBox : InnerTextBox + { + public bool AllowDecimals { get; init; } + + protected override bool CanAddCharacter(char character) + => char.IsAsciiDigit(character) || (AllowDecimals && character == '.'); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs new file mode 100644 index 0000000000..985eb74662 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -0,0 +1,238 @@ +// 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.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormTextBox : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private bool readOnly; + + public bool ReadOnly + { + get => readOnly; + set + { + readOnly = value; + + if (textBox.IsNotNull()) + updateState(); + } + } + + private CompositeDrawable? tabbableContentContainer; + + public CompositeDrawable? TabbableContentContainer + { + set + { + tabbableContentContainer = value; + + if (textBox.IsNotNull()) + textBox.TabbableContentContainer = tabbableContentContainer; + } + } + + public event TextBox.OnCommitHandler? OnCommit; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + public LocalisableString PlaceholderText { get; init; } + + private Box background = null!; + private Box flashLayer = null!; + private InnerTextBox textBox = null!; + private FormFieldCaption caption = null!; + private IFocusManager focusManager = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + textBox = CreateTextBox().With(t => + { + t.Anchor = Anchor.BottomRight; + t.Origin = Anchor.BottomRight; + t.RelativeSizeAxes = Axes.X; + t.Width = 1; + t.PlaceholderText = PlaceholderText; + t.Current = Current; + t.CommitOnFocusLost = true; + t.OnCommit += (textBox, newText) => + { + OnCommit?.Invoke(textBox, newText); + + if (!current.Disabled && !ReadOnly) + { + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + } + }; + t.OnInputError = () => + { + flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); + flashLayer.FadeOutFromOne(200, Easing.OutQuint); + }; + t.TabbableContentContainer = tabbableContentContainer; + }), + }, + }, + }; + } + + internal virtual InnerTextBox CreateTextBox() => new InnerTextBox(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + focusManager = GetContainingFocusManager()!; + textBox.Focused.BindValueChanged(_ => updateState()); + current.BindDisabledChanged(_ => updateState(), true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + focusManager.ChangeFocus(textBox); + return true; + } + + private void updateState() + { + bool disabled = Current.Disabled || ReadOnly; + + textBox.ReadOnly = disabled; + textBox.Alpha = 1; + + caption.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + if (!disabled) + { + BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; + + if (textBox.Focused.Value) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + else + { + background.Colour = colourProvider.Background4; + } + } + + internal partial class InnerTextBox : OsuTextBox + { + public BindableBool Focused { get; } = new BindableBool(); + + public Action? OnInputError { get; set; } + + protected override float LeftRightPadding => 0; + + [BackgroundDependencyLoader] + private void load() + { + Height = 16; + TextContainer.Height = 1; + Masking = false; + BackgroundUnfocused = BackgroundFocused = BackgroundCommit = Colour4.Transparent; + } + + protected override SpriteText CreatePlaceholder() => base.CreatePlaceholder().With(t => t.Margin = default); + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + Focused.Value = true; + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + Focused.Value = false; + } + + protected override void NotifyInputError() + { + PlayFeedbackSample(FeedbackSampleType.TextInvalid); + // base call intentionally suppressed + OnInputError?.Invoke(); + } + } + } +} From e1b763ff0db4de395f93171aa567cb41e7dd9171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 11:21:59 +0200 Subject: [PATCH 2/2] Apply review suggestions wrt border appearance --- osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 985eb74662..044576c635 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -80,6 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; + CornerExponent = 2.5f; InternalChildren = new Drawable[] { @@ -178,7 +179,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (!disabled) { - BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; if (textBox.Focused.Value)