diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index 5c5ca0d763..61489c73fc 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -6,9 +6,12 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osuTK; +using osuTK.Graphics; using osuTK.Input; namespace osu.Framework.Tests.Visual.UserInterface @@ -19,7 +22,7 @@ public class TestSceneTextBox : ManualInputManagerTestScene { typeof(BasicTextBox), typeof(TextBox), - typeof(PasswordTextBox) + typeof(BasicPasswordTextBox) }; private FillFlowContainer textBoxes; @@ -96,6 +99,13 @@ public void VariousTextBoxes() TabbableContentContainer = textBoxes }); + textBoxes.Add(new CustomTextBox + { + Text = @"Custom textbox", + Size = new Vector2(500, 30), + TabbableContentContainer = textBoxes + }); + FillFlowContainer otherTextBoxes = new FillFlowContainer { Direction = FillDirection.Vertical, @@ -118,7 +128,7 @@ public void VariousTextBoxes() TabbableContentContainer = otherTextBoxes }); - otherTextBoxes.Add(new PasswordTextBox + otherTextBoxes.Add(new BasicPasswordTextBox { PlaceholderText = @"Password textbox", Text = "Secret ;)", @@ -260,5 +270,73 @@ private class NumberTextBox : BasicTextBox { protected override bool CanAddCharacter(char character) => char.IsNumber(character); } + + private class CustomTextBox : BasicTextBox + { + protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, CalculatedTextSize); + + private class ScalingText : CompositeDrawable + { + private readonly SpriteText text; + + public ScalingText(char c, float textSize) + { + AddInternal(text = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = c.ToString(), + Font = FrameworkFont.Condensed.With(size: textSize), + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Size = text.DrawSize; + } + + public override void Show() + { + text.Scale = Vector2.Zero; + text.FadeIn(200).ScaleTo(1, 200); + } + + public override void Hide() + { + text.Scale = Vector2.One; + text.ScaleTo(0, 200).FadeOut(200); + } + } + + protected override Caret CreateCaret() => new BorderCaret(); + + private class BorderCaret : Caret + { + private const float caret_width = 2; + + public BorderCaret() + { + RelativeSizeAxes = Axes.Y; + + Masking = true; + BorderColour = Color4.White; + BorderThickness = 3; + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent + }; + } + + public override void DisplayAt(Vector2 position, float? selectionWidth) + { + Position = position - Vector2.UnitX; + Width = selectionWidth + 1 ?? caret_width; + } + } + } } } diff --git a/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs similarity index 87% rename from osu.Framework/Graphics/UserInterface/PasswordTextBox.cs rename to osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs index f166f7b0f6..be59b25447 100644 --- a/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs @@ -5,7 +5,7 @@ namespace osu.Framework.Graphics.UserInterface { - public class PasswordTextBox : TextBox, ISuppressKeyEventLogging + public class BasicPasswordTextBox : BasicTextBox, ISuppressKeyEventLogging { protected virtual char MaskCharacter => '*'; diff --git a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs index 8face0a90a..e7beaaf47b 100644 --- a/osu.Framework/Graphics/UserInterface/BasicTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicTextBox.cs @@ -1,34 +1,187 @@ // 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.Extensions.Color4Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK; using osuTK.Graphics; namespace osu.Framework.Graphics.UserInterface { public class BasicTextBox : TextBox { - protected override float CaretWidth => 2; + protected virtual float CaretWidth => 2; - protected override Color4 SelectionColour => FrameworkColour.YellowGreen; + private const float caret_move_time = 60; + + protected virtual Color4 SelectionColour => FrameworkColour.YellowGreen; + + protected Color4 BackgroundCommit { get; set; } = FrameworkColour.Green; + + private Color4 backgroundFocused = new Color4(100, 100, 100, 255); + private Color4 backgroundUnfocused = new Color4(100, 100, 100, 120); + + private readonly Box background; + + protected Color4 BackgroundFocused + { + get => backgroundFocused; + set + { + backgroundFocused = value; + if (HasFocus) + background.Colour = value; + } + } + + protected Color4 BackgroundUnfocused + { + get => backgroundUnfocused; + set + { + backgroundUnfocused = value; + if (!HasFocus) + background.Colour = value; + } + } + + protected virtual Color4 InputErrorColour => Color4.Red; public BasicTextBox() { - CornerRadius = 0; + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + Colour = BackgroundUnfocused, + }); + BackgroundFocused = FrameworkColour.BlueGreen; BackgroundUnfocused = FrameworkColour.BlueGreenDark; - BackgroundCommit = FrameworkColour.Green; - TextFlow.Height = 0.75f; + TextContainer.Height = 0.75f; + } + + protected override void NotifyInputError() => background.FlashColour(InputErrorColour, 200); + + protected override void Commit() + { + base.Commit(); + + background.Colour = ReleaseFocusOnCommit ? BackgroundUnfocused : BackgroundFocused; + background.ClearTransforms(); + background.FlashColour(BackgroundCommit, 400); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + background.ClearTransforms(); + background.Colour = BackgroundFocused; + background.FadeColour(BackgroundUnfocused, 200, Easing.OutExpo); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + background.ClearTransforms(); + background.Colour = BackgroundUnfocused; + background.FadeColour(BackgroundFocused, 200, Easing.Out); } - protected override Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: CalculatedTextSize) }; + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new SpriteText { Text = c.ToString(), Font = FrameworkFont.Condensed.With(size: CalculatedTextSize) } + }; - protected override SpriteText CreatePlaceholder() => new SpriteText + protected override SpriteText CreatePlaceholder() => new FadingPlaceholderText { Colour = FrameworkColour.YellowGreen, Font = FrameworkFont.Condensed, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + X = CaretWidth, + }; + + public class FallingDownContainer : Container + { + public override void Show() + { + var col = (Color4)Colour; + this.FadeColour(col.Opacity(0)).FadeColour(col, caret_move_time * 2, Easing.Out); + } + + public override void Hide() + { + this.FadeOut(200); + this.MoveToY(DrawSize.Y, 200, Easing.InExpo); + } + } + + public class FadingPlaceholderText : SpriteText + { + public override void Show() => this.FadeIn(200); + + public override void Hide() => this.FadeOut(200); + } + + protected override Caret CreateCaret() => new BasicCaret + { + CaretWidth = CaretWidth, + SelectionColour = SelectionColour, }; + + public class BasicCaret : Caret + { + public BasicCaret() + { + RelativeSizeAxes = Axes.Y; + Size = new Vector2(1, 0.9f); + + Colour = Color4.Transparent; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Masking = true; + CornerRadius = 1; + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }; + } + + public override void Hide() => this.FadeOut(200); + + public float CaretWidth { get; set; } + + public Color4 SelectionColour { get; set; } + + public override void DisplayAt(Vector2 position, float? selectionWidth) + { + if (selectionWidth != null) + { + this.MoveTo(new Vector2(position.X, position.Y), 60, Easing.Out); + this.ResizeWidthTo(selectionWidth.Value + CaretWidth / 2, caret_move_time, Easing.Out); + this + .FadeTo(0.5f, 200, Easing.Out) + .FadeColour(SelectionColour, 200, Easing.Out); + } + else + { + this.MoveTo(new Vector2(position.X - CaretWidth / 2, position.Y), 60, Easing.Out); + this.ResizeWidthTo(CaretWidth, caret_move_time, Easing.Out); + this + .FadeColour(Color4.White, 200, Easing.Out) + .Loop(c => c.FadeTo(0.7f).FadeTo(0.4f, 500, Easing.InOutSine)); + } + } + } } } diff --git a/osu.Framework/Graphics/UserInterface/Caret.cs b/osu.Framework/Graphics/UserInterface/Caret.cs new file mode 100644 index 0000000000..02fd276d92 --- /dev/null +++ b/osu.Framework/Graphics/UserInterface/Caret.cs @@ -0,0 +1,21 @@ +// 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.Containers; +using osuTK; + +namespace osu.Framework.Graphics.UserInterface +{ + /// + /// A UI component generally used to show the current cursor location in a text edit field. + /// + public abstract class Caret : CompositeDrawable + { + /// + /// Request the caret be displayed at a particular location, with an optional selection length. + /// + /// The position (in parent space) where the caret should be displayed. + /// If a selection is active, the length (in parent space) of the selection. The caret should extend to display this selection to the user. + public abstract void DisplayAt(Vector2 position, float? selectionWidth); + } +} diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index be7a98e1ea..93f556017c 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -16,21 +16,17 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Platform; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Timing; namespace osu.Framework.Graphics.UserInterface { - public class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingHandler + public abstract class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingHandler { - protected FillFlowContainer TextFlow; - protected Box Background; - protected Drawable Caret; - protected Container TextContainer; + protected FillFlowContainer TextFlow { get; private set; } + protected Container TextContainer { get; private set; } public override bool HandleNonPositionalInput => HasFocus; @@ -39,10 +35,6 @@ public class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingH /// protected virtual float LeftRightPadding => 5; - protected virtual float CaretWidth => 3; - - private const float caret_move_time = 60; - public int? LengthLimit; /// @@ -66,35 +58,6 @@ public class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingH /// public virtual bool HandleLeftRightArrows => true; - private Color4 backgroundFocused = new Color4(100, 100, 100, 255); - private Color4 backgroundUnfocused = new Color4(100, 100, 100, 120); - - protected Color4 BackgroundCommit { get; set; } = new Color4(249, 90, 255, 200); - - protected Color4 BackgroundFocused - { - get => backgroundFocused; - set - { - backgroundFocused = value; - updateFocus(); - } - } - - protected Color4 BackgroundUnfocused - { - get => backgroundUnfocused; - set - { - backgroundUnfocused = value; - updateFocus(); - } - } - - protected virtual Color4 SelectionColour => new Color4(249, 90, 255, 255); - - protected virtual Color4 InputErrorColour => Color4.Red; - /// /// Check if a character can be added to this TextBox. /// @@ -117,26 +80,23 @@ protected Color4 BackgroundUnfocused public override bool CanBeTabbedTo => !ReadOnly; private ITextInputSource textInput; + private Clipboard clipboard; + private readonly Caret caret; + public delegate void OnCommitHandler(TextBox sender, bool newText); public OnCommitHandler OnCommit; private readonly Scheduler textUpdateScheduler = new Scheduler(); - public TextBox() + protected TextBox() { Masking = true; - CornerRadius = 3; Children = new Drawable[] { - Background = new Box - { - Colour = BackgroundUnfocused, - RelativeSizeAxes = Axes.Both, - }, TextContainer = new Container { AutoSizeAxes = Axes.X, @@ -144,10 +104,10 @@ public TextBox() Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Position = new Vector2(LeftRightPadding, 0), - Children = new[] + Children = new Drawable[] { - Placeholder = CreatePlaceholder().With(p => p.X = CaretWidth), - Caret = new DrawableCaret(), + Placeholder = CreatePlaceholder(), + caret = CreateCaret(), TextFlow = new FillFlowContainer { Anchor = Anchor.CentreLeft, @@ -161,6 +121,7 @@ public TextBox() }; Current.ValueChanged += e => { Text = e.NewValue; }; + caret.Hide(); } [BackgroundDependencyLoader] @@ -317,8 +278,6 @@ private void resetSelection() cursorAndLayout.Invalidate(); } - private void updateFocus() => Background.FadeColour(HasFocus ? BackgroundFocused : BackgroundUnfocused, Background.IsLoaded ? 200 : 0); - protected override void Dispose(bool isDisposing) { OnCommit = null; @@ -338,16 +297,15 @@ private void updateCursorAndLayout() textUpdateScheduler.Update(); - float caretWidth = CaretWidth; - - Vector2 cursorPos = Vector2.Zero; + float cursorPos = 0; if (text.Length > 0) - cursorPos.X = getPositionAt(selectionLeft) - CaretWidth / 2; + cursorPos = getPositionAt(selectionLeft); float cursorPosEnd = getPositionAt(selectionEnd); + float? selectionWidth = null; if (selectionLength > 0) - caretWidth = getPositionAt(selectionRight) - cursorPos.X; + selectionWidth = getPositionAt(selectionRight) - cursorPos; float cursorRelativePositionAxesInBox = (cursorPosEnd - textContainerPosX) / DrawWidth; @@ -362,29 +320,18 @@ private void updateCursorAndLayout() TextContainer.MoveToX(LeftRightPadding - textContainerPosX, 300, Easing.OutExpo); if (HasFocus) - { - Caret.ClearTransforms(); - Caret.MoveTo(cursorPos, 60, Easing.Out); - Caret.ResizeWidthTo(caretWidth, caret_move_time, Easing.Out); - - if (selectionLength > 0) - { - Caret - .FadeTo(0.5f, 200, Easing.Out) - .FadeColour(SelectionColour, 200, Easing.Out); - } - else - { - Caret - .FadeColour(Color4.White, 200, Easing.Out) - .Loop(c => c.FadeTo(0.7f).FadeTo(0.4f, 500, Easing.InOutSine)); - } - } + caret.DisplayAt(new Vector2(cursorPos, 0), selectionWidth); if (textAtLastLayout != text) Current.Value = text; + if (textAtLastLayout.Length == 0 || text.Length == 0) - Placeholder.FadeTo(text.Length == 0 ? 1 : 0, 200); + { + if (text.Length == 0) + Placeholder.Show(); + else + Placeholder.Hide(); + } textAtLastLayout = text; } @@ -497,8 +444,7 @@ private bool removeCharacterOrSelection(bool sound = true) // account for potentially altered height of textbox d.Y = TextFlow.BoundingBox.Y; - d.FadeOut(200); - d.MoveToY(d.DrawSize.Y, 200, Easing.InExpo); + d.Hide(); d.Expire(); } @@ -513,6 +459,11 @@ private bool removeCharacterOrSelection(bool sound = true) return true; } + /// + /// Creates a single character. Override and for custom behavior. + /// + /// The character that this should represent. + /// A that represents the character protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), Font = new FontUsage(size: CalculatedTextSize) }; protected virtual Drawable AddCharacterToFlow(char c) @@ -557,12 +508,11 @@ protected void InsertString(string text) if (ch == null) { - notifyInputError(); + NotifyInputError(); continue; } - var col = (Color4)ch.Colour; - ch.FadeColour(col.Opacity(0)).FadeColour(col, caret_move_time * 2, Easing.Out); + ch.Show(); } } @@ -576,7 +526,7 @@ private Drawable addCharacter(char c) if (text.Length + 1 > LengthLimit) { - notifyInputError(); + NotifyInputError(); return null; } @@ -590,18 +540,16 @@ private Drawable addCharacter(char c) return ch; } - private void notifyInputError() - { - if (Background.Alpha > 0) - Background.FlashColour(InputErrorColour, 200); - else - TextFlow.FlashColour(InputErrorColour, 200); - } + /// + /// Called whenever an invalid character has been entered + /// + protected abstract void NotifyInputError(); - protected virtual SpriteText CreatePlaceholder() => new SpriteText - { - Colour = Color4.Gray, - }; + /// + /// Creates a placeholder that shows whenever the textbox is empty. Override or for custom behavior. + /// + /// The placeholder + protected abstract SpriteText CreatePlaceholder(); protected SpriteText Placeholder; @@ -611,6 +559,8 @@ public string PlaceholderText set => Placeholder.Text = value; } + protected abstract Caret CreateCaret(); + private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -634,7 +584,10 @@ public virtual string Text lastCommitText = value ??= string.Empty; - Placeholder.FadeTo(value.Length == 0 ? 1 : 0); + if (value.Length == 0) + Placeholder.Show(); + else + Placeholder.Hide(); if (!IsLoaded) Current.Value = text = value; @@ -744,7 +697,7 @@ private void killFocus() manager.ChangeFocus(null); } - protected void Commit() + protected virtual void Commit() { if (ReleaseFocusOnCommit && HasFocus) { @@ -754,10 +707,6 @@ protected void Commit() return; } - Background.Colour = ReleaseFocusOnCommit ? BackgroundUnfocused : BackgroundFocused; - Background.ClearTransforms(); - Background.FlashColour(BackgroundCommit, 400); - audio.Samples.Get(@"Keyboard/key-confirm")?.Play(); OnCommit?.Invoke(this, hasNewComittableText); @@ -886,12 +835,7 @@ protected override void OnFocusLost(FocusLostEvent e) { unbindInput(); - Caret.ClearTransforms(); - Caret.FadeOut(200); - - Background.ClearTransforms(); - Background.FadeColour(BackgroundUnfocused, 200, Easing.OutExpo); - + caret.Hide(); cursorAndLayout.Invalidate(); if (CommitOnFocusLost) @@ -906,9 +850,7 @@ protected override void OnFocus(FocusEvent e) { bindInput(); - Background.ClearTransforms(); - Background.FadeColour(BackgroundFocused, 200, Easing.Out); - + caret.Show(); cursorAndLayout.Invalidate(); } @@ -997,27 +939,5 @@ private void onImeComposition(string s) } #endregion - - private class DrawableCaret : CompositeDrawable - { - public DrawableCaret() - { - RelativeSizeAxes = Axes.Y; - Size = new Vector2(1, 0.9f); - Alpha = 0; - Colour = Color4.Transparent; - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - - Masking = true; - CornerRadius = 1; - - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }; - } - } } }