diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2199c2cb8..1244c0bd5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] diff --git a/osu-framework.sln.DotSettings b/osu-framework.sln.DotSettings index 45f036361..31759a235 100644 --- a/osu-framework.sln.DotSettings +++ b/osu-framework.sln.DotSettings @@ -342,6 +342,7 @@ API ARGB BPM + CG FBO CCL GC diff --git a/osu.Framework.Android/osu.Framework.Android.csproj b/osu.Framework.Android/osu.Framework.Android.csproj index 77975c826..f2992c216 100644 --- a/osu.Framework.Android/osu.Framework.Android.csproj +++ b/osu.Framework.Android/osu.Framework.Android.csproj @@ -18,7 +18,6 @@ - diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs index 31f5d3800..6a7c741ce 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs @@ -525,7 +525,7 @@ namespace osu.Framework.Tests.Visual.Containers AddStep($"scroll to {position}", () => { scrollContainer.ScrollTo(position, false); - immediateScrollPosition = scrollContainer.Current; + immediateScrollPosition = (float)scrollContainer.Current; }); AddAssert($"immediately scrolled to {clampedTarget}", () => Precision.AlmostEquals(clampedTarget, immediateScrollPosition, 1)); diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs new file mode 100644 index 000000000..7b995fdfa --- /dev/null +++ b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs @@ -0,0 +1,149 @@ +// 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.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Containers +{ + public partial class TestSceneScrollContainerDoublePrecision : ManualInputManagerTestScene + { + private const float item_height = 5000; + private const int item_count = 8000; + + private ScrollContainer scrollContainer = null!; + + [SetUp] + public void Setup() => Schedule(Clear); + + [Test] + public void TestStandard() + { + AddStep("Create scroll container", () => + { + Add(scrollContainer = new BasicScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ScrollbarVisible = true, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f, 0.9f), + }); + + for (int i = 0; i < item_count; i++) + { + scrollContainer.Add(new BoxWithDouble + { + Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1), + RelativeSizeAxes = Axes.X, + Height = item_height, + Y = i * item_height, + }); + } + }); + + scrollIntoView(item_count - 2); + scrollIntoView(item_count - 1); + } + + [Test] + public void TestDoublePrecision() + { + AddStep("Create scroll container", () => + { + Add(scrollContainer = new DoubleScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ScrollbarVisible = true, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f, 0.9f), + }); + + for (int i = 0; i < item_count; i++) + { + scrollContainer.Add(new BoxWithDouble + { + Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1), + RelativeSizeAxes = Axes.X, + Height = item_height, + DoubleLocation = i * item_height, + }); + } + }); + + scrollIntoView(item_count - 2); + scrollIntoView(item_count - 1); + } + + private void scrollIntoView(int index) + { + AddStep($"scroll {index} into view", () => scrollContainer.ScrollIntoView(scrollContainer.ChildrenOfType().Skip(index).First())); + AddUntilStep($"{index} is visible", () => !scrollContainer.ChildrenOfType().Skip(index).First().IsMaskedAway); + } + + public partial class DoubleScrollContainer : BasicScrollContainer + { + private readonly Container layoutContent; + + public override void Add(Drawable drawable) + { + if (drawable is not BoxWithDouble boxWithDouble) + throw new InvalidOperationException(); + + Add(boxWithDouble); + } + + public void Add(BoxWithDouble drawable) + { + if (drawable is not BoxWithDouble boxWithDouble) + throw new InvalidOperationException(); + + layoutContent.Height = (float)Math.Max(layoutContent.Height, boxWithDouble.DoubleLocation + boxWithDouble.DrawHeight); + layoutContent.Add(drawable); + } + + public DoubleScrollContainer() + { + // Managing our own custom layout within ScrollContent causes feedback with internal ScrollContainer calculations, + // so we must maintain one level of separation from ScrollContent. + base.Add(layoutContent = new Container + { + RelativeSizeAxes = Axes.X, + }); + } + + public override double GetChildPosInContent(Drawable d, Vector2 offset) + { + if (d is not BoxWithDouble boxWithDouble) + return base.GetChildPosInContent(d, offset); + + return boxWithDouble.DoubleLocation + offset.X; + } + + protected override void ApplyCurrentToContent() + { + Debug.Assert(ScrollDirection == Direction.Vertical); + + double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; + + foreach (var d in layoutContent) + d.Y = (float)(d.DoubleLocation + scrollableExtent); + } + } + + public partial class BoxWithDouble : Box + { + public double DoubleLocation { get; set; } + } + } +} diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneDelayedLoadWrapper.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneDelayedLoadWrapper.cs index c031f144c..123e47ed8 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneDelayedLoadWrapper.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneDelayedLoadWrapper.cs @@ -101,8 +101,10 @@ namespace osu.Framework.Tests.Visual.Drawables AddUntilStep("repeating schedulers removed", () => !scroll.Scheduler.HasPendingTasks); } + // Fails once in a blue moon due to loose (but maybe-not-loose-enough) timing requirements. If we break things, it will fail every time so this is fine. [TestCase(false)] [TestCase(true)] + [FlakyTest] public void TestManyChildrenFunction(bool instant) { AddStep("create children", () => diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs index 9b907e175..8b7de51eb 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs @@ -467,7 +467,7 @@ namespace osu.Framework.Tests.Visual.UserInterface private partial class TestRearrangeableList : BasicRearrangeableListContainer { - public float ScrollPosition => ScrollContainer.Current; + public float ScrollPosition => (float)ScrollContainer.Current; public new IReadOnlyDictionary> ItemMap => base.ItemMap; diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs index 1242f984f..c59193f54 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs @@ -95,7 +95,15 @@ namespace osu.Framework.Tests.Visual.UserInterface textBoxes.Add(new CustomTextBox { - Text = @"Custom textbox", + PlaceholderText = "Custom textbox", + Size = new Vector2(500, 30), + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new BasicTextBox + { + InputProperties = new TextInputProperties(TextInputType.Text, AutoCapitalisation: true), + Text = "Auto-capitalised textbox", Size = new Vector2(500, 30), TabbableContentContainer = textBoxes }); @@ -122,8 +130,9 @@ namespace osu.Framework.Tests.Visual.UserInterface TabbableContentContainer = otherTextBoxes }); - otherTextBoxes.Add(new BasicPasswordTextBox + otherTextBoxes.Add(new BasicTextBox { + InputProperties = new TextInputProperties(TextInputType.Password), PlaceholderText = @"Password textbox", Text = "Secret ;)", Size = new Vector2(500, 30), @@ -169,12 +178,13 @@ namespace osu.Framework.Tests.Visual.UserInterface [Test] public void TestNumbersOnly() { - NumberTextBox numbers = null; + BasicTextBox numbers = null; AddStep("add number textbox", () => { - textBoxes.Add(numbers = new NumberTextBox + textBoxes.Add(numbers = new BasicTextBox { + InputProperties = new TextInputProperties(TextInputType.Number), PlaceholderText = @"Only numbers", Size = new Vector2(500, 30), TabbableContentContainer = textBoxes @@ -800,6 +810,40 @@ namespace osu.Framework.Tests.Visual.UserInterface AddAssert("nothing selected", () => textBox.SelectedText == string.Empty); } + [Test] + public void TestTextChangedDuringDoubleClickDrag() + { + InsertableTextBox textBox = null; + + AddStep("add textbox", () => + { + textBoxes.Add(textBox = new InsertableTextBox + { + Size = new Vector2(300, 40), + Text = "initial text", + }); + }); + + AddStep("click on textbox", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("set text", () => textBox.Text = "aaaaaaaaaaaaaaaaaaaa"); + + AddStep("select word", () => + { + InputManager.Click(MouseButton.Left); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("insert text", () => textBox.InsertString("a")); + AddAssert("text overwritten", () => textBox.Text == "a"); + AddStep("start drag", () => InputManager.MoveMouseTo(textBox, new Vector2(-50, 0))); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } + [Test] public void TestSelectAll() { @@ -1042,13 +1086,6 @@ namespace osu.Framework.Tests.Visual.UserInterface public new void InsertString(string text) => base.InsertString(text); } - private partial class NumberTextBox : BasicTextBox - { - protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); - - protected override bool AllowIme => false; - } - private partial class CustomTextBox : BasicTextBox { protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, FontSize); diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs index 643924de6..d3211bfd6 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs @@ -206,7 +206,7 @@ namespace osu.Framework.Tests.Visual.UserInterface AddAssert("text input not deactivated", () => textInput.DeactivationQueue.Count == 0); AddAssert("text input not activated again", () => textInput.ActivationQueue.Count == 0); - AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() && textInput.EnsureActivatedQueue.Count == 0); + AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() != default && textInput.EnsureActivatedQueue.Count == 0); AddStep("click deselection", () => { @@ -217,7 +217,7 @@ namespace osu.Framework.Tests.Visual.UserInterface AddAssert("text input not deactivated", () => textInput.DeactivationQueue.Count == 0); AddAssert("text input not activated again", () => textInput.ActivationQueue.Count == 0); - AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() && textInput.EnsureActivatedQueue.Count == 0); + AddAssert("text input ensure activated", () => textInput.EnsureActivatedQueue.Dequeue() != default && textInput.EnsureActivatedQueue.Count == 0); AddStep("click-drag selection", () => { @@ -500,7 +500,7 @@ namespace osu.Framework.Tests.Visual.UserInterface AddStep("add second textbox", () => textInputContainer.Add(secondTextBox = new EventQueuesTextBox { - ImeAllowed = allowIme, + InputProperties = new TextInputProperties(TextInputType.Text, allowIme), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, CommitOnFocusLost = true, @@ -517,7 +517,7 @@ namespace osu.Framework.Tests.Visual.UserInterface AddAssert("text input not deactivated", () => textInput.DeactivationQueue.Count == 0); AddAssert("text input not activated again", () => textInput.ActivationQueue.Count == 0); - AddAssert($"text input ensure activated {(allowIme ? "with" : "without")} IME", () => textInput.EnsureActivatedQueue.Dequeue() == allowIme && textInput.EnsureActivatedQueue.Count == 0); + AddAssert($"text input ensure activated {(allowIme ? "with" : "without")} IME", () => textInput.EnsureActivatedQueue.Dequeue().AllowIme == allowIme && textInput.EnsureActivatedQueue.Count == 0); AddStep("commit text", () => InputManager.Key(Key.Enter)); AddAssert("text input deactivated", () => textInput.DeactivationQueue.Dequeue()); @@ -574,10 +574,6 @@ namespace osu.Framework.Tests.Visual.UserInterface public partial class EventQueuesTextBox : TestSceneTextBox.InsertableTextBox { - public bool ImeAllowed { get; set; } = true; - - protected override bool AllowIme => ImeAllowed; - public readonly Queue InputErrorQueue = new Queue(); public readonly Queue UserConsumedTextQueue = new Queue(); public readonly Queue UserRemovedTextQueue = new Queue(); diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneTextInputProperties.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextInputProperties.cs new file mode 100644 index 000000000..3c45176d7 --- /dev/null +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneTextInputProperties.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 System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Testing; +using osuTK; + +namespace osu.Framework.Tests.Visual.UserInterface +{ + public partial class TestSceneTextInputProperties : FrameworkTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create text boxes", () => + { + FillFlowContainer flow; + Child = new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.9f, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(20, 13), + } + }; + + foreach (var textInputType in Enum.GetValues()) + { + flow.Add(new BasicTextBox + { + TabbableContentContainer = flow, + RelativeSizeAxes = Axes.X, + Height = 40, + Width = 0.45f, + PlaceholderText = $"{textInputType} (allow IME)", + InputProperties = new TextInputProperties + { + Type = textInputType, + AllowIme = true + }, + }); + flow.Add(new BasicTextBox + { + TabbableContentContainer = flow, + RelativeSizeAxes = Axes.X, + Height = 40, + Width = 0.45f, + PlaceholderText = $"{textInputType} (no IME)", + InputProperties = new TextInputProperties + { + Type = textInputType, + AllowIme = false + }, + }); + } + }); + } + } +} diff --git a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs index 45253e55d..174cf2193 100644 --- a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs +++ b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs @@ -2,22 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; -using Accelerate; -using CoreGraphics; using Foundation; -using ObjCRuntime; -using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform.Apple; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; using UIKit; namespace osu.Framework.iOS.Graphics.Textures { - public class IOSTextureLoaderStore : TextureLoaderStore + internal class IOSTextureLoaderStore : AppleTextureLoaderStore { public IOSTextureLoaderStore(IResourceStore store) : base(store) @@ -26,81 +20,21 @@ namespace osu.Framework.iOS.Graphics.Textures protected override unsafe Image ImageFromStream(Stream stream) { - using (var nativeData = NSData.FromStream(stream)) + using (new NSAutoreleasePool()) { - if (nativeData == null) + int length = (int)(stream.Length - stream.Position); + var nativeData = NSMutableData.FromLength(length); + + var bytesSpan = new Span(nativeData.MutableBytes.ToPointer(), length); + stream.ReadExactly(bytesSpan); + + using var uiImage = UIImage.LoadFromData(nativeData); + if (uiImage == null) throw new ArgumentException($"{nameof(Image)} could not be created from {nameof(stream)}."); - using (var uiImage = UIImage.LoadFromData(nativeData)) - { - if (uiImage == null) throw new ArgumentException($"{nameof(Image)} could not be created from {nameof(stream)}."); - - int width = (int)uiImage.Size.Width; - int height = (int)uiImage.Size.Height; - - var format = new vImage_CGImageFormat - { - BitsPerComponent = 8, - BitsPerPixel = 32, - ColorSpace = CGColorSpace.CreateDeviceRGB().Handle, - // notably, iOS generally uses premultiplied alpha when rendering image to pixels via CGBitmapContext or otherwise, - // but vImage offers using straight alpha directly without any conversion from our side (by specifying Last instead of PremultipliedLast). - BitmapInfo = (CGBitmapFlags)CGImageAlphaInfo.Last, - Decode = null, - RenderingIntent = CGColorRenderingIntent.Default, - }; - - vImageBuffer accelerateImage = default; - - // perform initial call to retrieve preferred alignment and bytes-per-row values for the given image dimensions. - nuint alignment = (nuint)vImageBuffer_Init(&accelerateImage, (uint)height, (uint)width, 32, vImageFlags.NoAllocate); - Debug.Assert(alignment > 0); - - // allocate aligned memory region to contain image pixel data. - int bytesPerRow = accelerateImage.BytesPerRow; - int bytesCount = bytesPerRow * accelerateImage.Height; - accelerateImage.Data = (IntPtr)NativeMemory.AlignedAlloc((nuint)bytesCount, alignment); - - var result = vImageBuffer_InitWithCGImage(&accelerateImage, &format, null, uiImage.CGImage!.Handle, vImageFlags.NoAllocate); - Debug.Assert(result == vImageError.NoError); - - var image = new Image(width, height); - byte* data = (byte*)accelerateImage.Data; - - for (int i = 0; i < height; i++) - { - var imageRow = image.DangerousGetPixelRowMemory(i); - var dataRow = new ReadOnlySpan(&data[bytesPerRow * i], width); - dataRow.CopyTo(imageRow.Span); - } - - NativeMemory.AlignedFree(accelerateImage.Data.ToPointer()); - return image; - } + var cgImage = new Platform.Apple.Native.CGImage(uiImage.CGImage!.Handle); + return ImageFromCGImage(cgImage); } } - - #region Accelerate API - - [DllImport(Constants.AccelerateLibrary)] - private static extern unsafe vImageError vImageBuffer_Init(vImageBuffer* buf, uint height, uint width, uint pixelBits, vImageFlags flags); - - [DllImport(Constants.AccelerateLibrary)] - private static extern unsafe vImageError vImageBuffer_InitWithCGImage(vImageBuffer* buf, vImage_CGImageFormat* format, nfloat* backgroundColour, NativeHandle image, vImageFlags flags); - - // ReSharper disable once InconsistentNaming - [StructLayout(LayoutKind.Sequential)] - public unsafe struct vImage_CGImageFormat - { - public uint BitsPerComponent; - public uint BitsPerPixel; - public NativeHandle ColorSpace; - public CGBitmapFlags BitmapInfo; - public uint Version; - public nfloat* Decode; - public CGColorRenderingIntent RenderingIntent; - } - - #endregion } } diff --git a/osu.Framework.iOS/IOSFilePresenter.cs b/osu.Framework.iOS/IOSFilePresenter.cs index b697e897e..e69e88ade 100644 --- a/osu.Framework.iOS/IOSFilePresenter.cs +++ b/osu.Framework.iOS/IOSFilePresenter.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using CoreGraphics; using Foundation; using UIKit; using UniformTypeIdentifiers; @@ -26,16 +27,24 @@ namespace osu.Framework.iOS if (documentInteraction.PresentPreview(true)) return true; + // Since file menus on iPadOS appear in a popover-like style, UIDocumentInteractionController requires specifying + // a rectangle in the present functions to display the menu as a popover around that rectangle. + // Ultimately, we want to be given a rectangle by the game so the popover doesn't look out of place, + // but for the time being, specify CGRectZero to make the popover display at the top left. var gameView = window.ViewController.View!; - return documentInteraction.PresentOpenInMenu(gameView.Bounds, gameView, true); + return documentInteraction.PresentOpenInMenu(CGRect.Empty, gameView, true); } public bool PresentFile(string filename) { setupViewController(filename); + // Since file menus on iPadOS appear in a popover-like style, UIDocumentInteractionController requires specifying + // a rectangle in the present functions to display the menu as a popover around that rectangle. + // Ultimately, we want to be given a rectangle by the game so the popover doesn't look out of place, + // but for the time being, specify CGRectZero to make the popover display at the top left. var gameView = window.ViewController.View!; - return documentInteraction.PresentOptionsMenu(gameView.Bounds, gameView, true); + return documentInteraction.PresentOptionsMenu(CGRect.Empty, gameView, true); } private void setupViewController(string filename) diff --git a/osu.Framework.iOS/IOSGameHost.cs b/osu.Framework.iOS/IOSGameHost.cs index 94b95ff14..91d860816 100644 --- a/osu.Framework.iOS/IOSGameHost.cs +++ b/osu.Framework.iOS/IOSGameHost.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Foundation; using osu.Framework.Configuration; using osu.Framework.Extensions; @@ -11,6 +12,8 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Mouse; using osu.Framework.IO.Stores; using osu.Framework.iOS.Graphics.Textures; using osu.Framework.iOS.Graphics.Video; @@ -85,6 +88,20 @@ namespace osu.Framework.iOS public override VideoDecoder CreateVideoDecoder(Stream stream) => new IOSVideoDecoder(Renderer, stream); + protected override IEnumerable CreateAvailableInputHandlers() + { + var handlers = base.CreateAvailableInputHandlers(); + + foreach (var h in handlers.OfType()) + { + // Similar to macOS, "relative mode" is also broken on iOS. + h.UseRelativeMode.Value = false; + h.UseRelativeMode.Default = false; + } + + return handlers; + } + public override ISystemFileSelector? CreateSystemFileSelector(string[] allowedExtensions) { IOSFileSelector? selector = null; diff --git a/osu.Framework/Game.cs b/osu.Framework/Game.cs index 1f7a8e827..30d048699 100644 --- a/osu.Framework/Game.cs +++ b/osu.Framework/Game.cs @@ -128,6 +128,20 @@ namespace osu.Framework protected sealed override void AddInternal(Drawable drawable) => throw new InvalidOperationException($"Use {nameof(Add)} or {nameof(Content)} instead."); + /// + /// The earliest point of entry during starting execution of a game. + /// This should be used to set up any low level tasks such as exception handling. + /// + /// + /// At this point in execution, only and are guaranteed to be valid for use. + /// They are provided as and respectively for convenience. + /// + /// The default game storage. + /// The default cache storage. + public virtual void SetupLogging(Storage gameStorage, Storage cacheStorage) + { + } + /// /// As Load is run post host creation, you can override this method to alter properties of the host before it makes itself visible to the user. /// diff --git a/osu.Framework/Graphics/Containers/ScrollContainer.cs b/osu.Framework/Graphics/Containers/ScrollContainer.cs index dbd89fed1..eb2f2de27 100644 --- a/osu.Framework/Graphics/Containers/ScrollContainer.cs +++ b/osu.Framework/Graphics/Containers/ScrollContainer.cs @@ -118,7 +118,7 @@ namespace osu.Framework.Graphics.Containers /// /// The current scroll position. /// - public float Current { get; private set; } + public double Current { get; private set; } /// /// The target scroll position which is exponentially approached by current via a rate of distance decay. @@ -126,12 +126,12 @@ namespace osu.Framework.Graphics.Containers /// /// When not animating scroll position, this will always be equal to . /// - public float Target { get; private set; } + public double Target { get; private set; } /// /// The maximum distance that can be scrolled in the scroll direction. /// - public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0); + public double ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0); /// /// The maximum distance that the scrollbar can move in the scroll direction. @@ -139,14 +139,14 @@ namespace osu.Framework.Graphics.Containers /// /// May not be accurate to actual display of scrollbar if or are overridden. /// - protected float ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0); + protected double ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0); /// /// Clamp a value to the available scroll range. /// /// The value to clamp. /// An extension value beyond the normal extent. - protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); + protected double Clamp(double position, double extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); protected override Container Content => ScrollContent; @@ -345,8 +345,8 @@ namespace osu.Framework.Graphics.Containers Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition); - float scrollOffset = -childDelta[ScrollDim]; - float clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target); + double scrollOffset = -childDelta[ScrollDim]; + double clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target); // If we are dragging past the extent of the scrollable area, half the offset // such that the user can feel it. @@ -418,13 +418,13 @@ namespace osu.Framework.Graphics.Containers /// Immediately offsets the current and target scroll position. /// /// The scroll offset. - public void OffsetScrollPosition(float offset) + public virtual void OffsetScrollPosition(double offset) { Target += offset; Current += offset; } - private void scrollByOffset(float value, bool animated, double distanceDecay = float.PositiveInfinity) => + private void scrollByOffset(double value, bool animated, double distanceDecay = float.PositiveInfinity) => OnUserScroll(Target + value, animated, distanceDecay); /// @@ -454,7 +454,7 @@ namespace osu.Framework.Graphics.Containers /// /// The amount by which we should scroll. /// Whether to animate the movement. - public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated); + public void ScrollBy(double offset, bool animated = true) => scrollTo(Target + offset, animated); /// /// Handle a scroll to an absolute position from a user input. @@ -462,7 +462,7 @@ namespace osu.Framework.Graphics.Containers /// The position to scroll to. /// Whether to animate the movement. /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . - protected virtual void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) => + protected virtual void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) => ScrollTo(value, animated, distanceDecay); /// @@ -471,9 +471,9 @@ namespace osu.Framework.Graphics.Containers /// The position to scroll to. /// Whether to animate the movement. /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . - public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); + public void ScrollTo(double value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); - private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity) + private void scrollTo(double value, bool animated, double distanceDecay = double.PositiveInfinity) { Target = Clamp(value, ClampExtension); @@ -497,11 +497,11 @@ namespace osu.Framework.Graphics.Containers /// Whether to animate the movement. public void ScrollIntoView(Drawable d, bool animated = true) { - float childPos0 = GetChildPosInContent(d); - float childPos1 = GetChildPosInContent(d, d.DrawSize); + double childPos0 = GetChildPosInContent(d); + double childPos1 = GetChildPosInContent(d, d.DrawSize); - float minPos = Math.Min(childPos0, childPos1); - float maxPos = Math.Max(childPos0, childPos1); + double minPos = Math.Min(childPos0, childPos1); + double maxPos = Math.Max(childPos0, childPos1); if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) ScrollTo(minPos, animated); @@ -515,14 +515,14 @@ namespace osu.Framework.Graphics.Containers /// The child to get the position from. /// Positional offset in the child's space. /// The position of the child. - public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim]; + public virtual double GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim]; /// /// Determines the position of a child in the content. /// /// The child to get the position from. /// The position of the child. - public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); + public double GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); private void updatePosition() { @@ -544,15 +544,15 @@ namespace osu.Framework.Graphics.Containers localDistanceDecay = distance_decay_clamping * 2; // Lastly, we gradually nudge the target towards valid bounds. - Target = (float)Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); + Target = Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); - float clampedTarget = Clamp(Target); + double clampedTarget = Clamp(Target); if (Precision.AlmostEquals(clampedTarget, Target)) Target = clampedTarget; } // Exponential interpolation between the target and our current scroll position. - Current = (float)Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); + Current = Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely. if (Precision.AlmostEquals(Current, Target)) @@ -578,15 +578,27 @@ namespace osu.Framework.Graphics.Containers } if (ScrollDirection == Direction.Horizontal) - { Scrollbar.X = ToScrollbarPosition(Current); - ScrollContent.X = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.X; - } else - { Scrollbar.Y = ToScrollbarPosition(Current); - ScrollContent.Y = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; - } + + ApplyCurrentToContent(); + } + + /// + /// This is the final internal step of updating the scroll container, which takes + /// and applies it to in order to + /// correctly offset children. + /// + /// Overriding this method can be used to inhibit this default behaviour, to for instance + /// redirect the positioning to another container or change the way it is applied. + /// + protected virtual void ApplyCurrentToContent() + { + if (ScrollDirection == Direction.Horizontal) + ScrollContent.X = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.X)); + else + ScrollContent.Y = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y)); } /// @@ -594,12 +606,12 @@ namespace osu.Framework.Graphics.Containers /// /// The absolute scroll position (e.g. ). /// The scrollbar position. - protected virtual float ToScrollbarPosition(float scrollPosition) + protected virtual float ToScrollbarPosition(double scrollPosition) { if (Precision.AlmostEquals(0, ScrollableExtent)) return 0; - return ScrollbarMovementExtent * (scrollPosition / ScrollableExtent); + return (float)(ScrollbarMovementExtent * (scrollPosition / ScrollableExtent)); } /// @@ -612,7 +624,7 @@ namespace osu.Framework.Graphics.Containers if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) return 0; - return ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent); + return (float)(ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent)); } /// diff --git a/osu.Framework/Graphics/Containers/SearchContainer.cs b/osu.Framework/Graphics/Containers/SearchContainer.cs index 47edd3dd6..3f8ad7f87 100644 --- a/osu.Framework/Graphics/Containers/SearchContainer.cs +++ b/osu.Framework/Graphics/Containers/SearchContainer.cs @@ -120,7 +120,7 @@ namespace osu.Framework.Graphics.Containers private void performFilter() { - string[] terms = (searchTerm ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + string[] terms = (searchTerm ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries); matchSubTree(this, terms, terms.Length > 0, allowNonContiguousMatching); } diff --git a/osu.Framework/Graphics/DrawableExtensions.cs b/osu.Framework/Graphics/DrawableExtensions.cs index ec61b220a..be6194ea4 100644 --- a/osu.Framework/Graphics/DrawableExtensions.cs +++ b/osu.Framework/Graphics/DrawableExtensions.cs @@ -49,7 +49,7 @@ namespace osu.Framework.Graphics /// The to be checked. /// The root to be checked against. /// Whether the drawable was rooted. - internal static bool IsRootedAt(this Drawable? drawable, Drawable root) + public static bool IsRootedAt(this Drawable? drawable, Drawable root) { if (drawable == root) return true; diff --git a/osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs b/osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs deleted file mode 100644 index b0791317b..000000000 --- a/osu.Framework/Graphics/UserInterface/BasicPasswordTextBox.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Input; - -namespace osu.Framework.Graphics.UserInterface -{ - public partial class BasicPasswordTextBox : BasicTextBox, ISuppressKeyEventLogging - { - protected virtual char MaskCharacter => '*'; - - protected override bool AllowClipboardExport => false; - - protected override bool AllowWordNavigation => false; - - protected override bool AllowIme => false; - - protected override Drawable AddCharacterToFlow(char c) => base.AddCharacterToFlow(MaskCharacter); - } -} diff --git a/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs index c5e3f9d2c..a23df45f9 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownSearchBar.cs @@ -192,20 +192,20 @@ namespace osu.Framework.Graphics.UserInterface platformSource.OnImeResult += TriggerImeResult; } - protected override void ActivateTextInput(bool allowIme) + protected override void ActivateTextInput(TextInputProperties properties) { - base.ActivateTextInput(allowIme); + base.ActivateTextInput(properties); if (allowTextInput) - platformSource.Activate(allowIme, imeRectangle ?? RectangleF.Empty); + platformSource.Activate(properties, imeRectangle ?? RectangleF.Empty); } - protected override void EnsureTextInputActivated(bool allowIme) + protected override void EnsureTextInputActivated(TextInputProperties properties) { - base.EnsureTextInputActivated(allowIme); + base.EnsureTextInputActivated(properties); if (allowTextInput) - platformSource.EnsureActivated(allowIme, imeRectangle); + platformSource.EnsureActivated(properties, imeRectangle); } protected override void DeactivateTextInput() diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index e825299cd..888f2eeb1 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -29,13 +29,18 @@ using osuTK.Input; namespace osu.Framework.Graphics.UserInterface { - public abstract partial class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingHandler + public abstract partial class TextBox : TabbableContainer, IHasCurrentValue, IKeyBindingHandler, ICanSuppressKeyEventLogging { protected FillFlowContainer TextFlow { get; private set; } protected Container TextContainer { get; private set; } public override bool HandleNonPositionalInput => HasFocus; + /// + /// A character displayed whenever the type of text input set by is hidden. + /// + protected virtual char MaskCharacter => '*'; + /// /// Padding to be used within the TextContainer. Requires special handling due to the sideways scrolling of text content. /// @@ -50,12 +55,14 @@ namespace osu.Framework.Graphics.UserInterface /// /// Whether clipboard copying functionality is allowed. /// - protected virtual bool AllowClipboardExport => true; + protected virtual bool AllowClipboardExport => !InputProperties.Type.IsPassword(); /// /// Whether seeking to word boundaries is allowed. /// - protected virtual bool AllowWordNavigation => true; + protected virtual bool AllowWordNavigation => !InputProperties.Type.IsPassword(); + + bool ICanSuppressKeyEventLogging.SuppressKeyEventLogging => InputProperties.Type.IsPassword(); /// /// Represents the left/right selection coordinates of the word double clicked on when dragging. @@ -67,18 +74,14 @@ namespace osu.Framework.Graphics.UserInterface /// public virtual bool HandleLeftRightArrows => true; - /// - /// Whether to allow IME input when this text box has input focus. - /// - /// - /// This is just a hint to the native implementation, some might respect this, - /// while others will ignore and always have the IME (dis)allowed. - /// - /// - /// Useful for situations where IME input is not wanted, such as for passwords, numbers, or romanised text. - /// + [Obsolete($"Use {nameof(InputProperties)} instead.")] // can be removed 20250506 protected virtual bool AllowIme => true; + /// + /// A set of properties to consider when interacting with this . + /// + public TextInputProperties InputProperties { get; init; } + /// /// Check if a character can be added to this TextBox. /// @@ -87,9 +90,14 @@ namespace osu.Framework.Graphics.UserInterface protected virtual bool CanAddCharacter(char character) => true; /// - /// Private helper for , additionally requiring that the character is not a control character. + /// Private helper for , additionally requiring that the character is not a control character and obeys . /// - private bool canAddCharacter(char character) => !char.IsControl(character) && CanAddCharacter(character); + private bool canAddCharacter(char character) + { + return !char.IsControl(character) + && (!InputProperties.Type.IsNumerical() || char.IsAsciiDigit(character)) + && CanAddCharacter(character); + } private bool readOnly; @@ -158,6 +166,10 @@ namespace osu.Framework.Graphics.UserInterface protected TextBox() { +#pragma warning disable CS0618 // Type or member is obsolete + InputProperties = new TextInputProperties(TextInputType.Text, AllowIme); +#pragma warning restore CS0618 // Type or member is obsolete + Masking = true; Children = new Drawable[] @@ -773,6 +785,7 @@ namespace osu.Framework.Graphics.UserInterface TextFlow.ChangeChildDepth(TextFlow[i], getDepthForCharacterIndex(i)); selectionStart = selectionEnd = removeStart; + doubleClickWord = null; endTextChange(beganChange); cursorAndLayout.Invalidate(); @@ -789,6 +802,9 @@ namespace osu.Framework.Graphics.UserInterface protected virtual Drawable AddCharacterToFlow(char c) { + if (InputProperties.Type.IsPassword()) + c = MaskCharacter; + // Remove all characters to the right and store them in a local list, // such that their depth can be updated. List charsRight = new List(); @@ -1339,7 +1355,7 @@ namespace osu.Framework.Graphics.UserInterface protected override bool OnClick(ClickEvent e) { if (!ReadOnly && textInputBound) - textInput.EnsureActivated(AllowIme); + textInput.EnsureActivated(InputProperties); return !ReadOnly; } @@ -1366,7 +1382,7 @@ namespace osu.Framework.Graphics.UserInterface if (textInputBound) { - textInput.EnsureActivated(AllowIme); + textInput.EnsureActivated(InputProperties); return; } @@ -1374,9 +1390,9 @@ namespace osu.Framework.Graphics.UserInterface // We don't deactivate and activate, but instead keep text input active during the focus handoff, so that virtual keyboards on phones don't flicker. if (previous?.textInput == textInput) - textInput.EnsureActivated(AllowIme, ScreenSpaceDrawQuad.AABBFloat); + textInput.EnsureActivated(InputProperties, ScreenSpaceDrawQuad.AABBFloat); else - textInput.Activate(AllowIme, ScreenSpaceDrawQuad.AABBFloat); + textInput.Activate(InputProperties, ScreenSpaceDrawQuad.AABBFloat); textInput.OnTextInput += handleTextInput; textInput.OnImeComposition += handleImeComposition; diff --git a/osu.Framework/Input/ButtonEventManager.cs b/osu.Framework/Input/ButtonEventManager.cs index b10ef23a2..ab8b5e492 100644 --- a/osu.Framework/Input/ButtonEventManager.cs +++ b/osu.Framework/Input/ButtonEventManager.cs @@ -133,9 +133,18 @@ namespace osu.Framework.Input } if (handledBy != null) - Logger.Log($"{e} handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + { + Logger.Log(SuppressLoggingEventInformation(handledBy) + ? $"{e.GetType().Name} handled by {handledBy}." + : $"{e} handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + } return handledBy; } + + /// + /// Whether information about the event should be suppressed from logging for the given drawable. + /// + protected virtual bool SuppressLoggingEventInformation(Drawable drawable) => false; } } diff --git a/osu.Framework/Input/ISuppressKeyEventLogging.cs b/osu.Framework/Input/ICanSuppressKeyEventLogging.cs similarity index 51% rename from osu.Framework/Input/ISuppressKeyEventLogging.cs rename to osu.Framework/Input/ICanSuppressKeyEventLogging.cs index 686ab71a9..02f6f4764 100644 --- a/osu.Framework/Input/ISuppressKeyEventLogging.cs +++ b/osu.Framework/Input/ICanSuppressKeyEventLogging.cs @@ -4,10 +4,14 @@ namespace osu.Framework.Input { /// - /// Marker interface which suppresses logging of keyboard input events. + /// An interface which suppresses logging of keyboard input events. /// Useful for password fields, where user input should not be logged. /// - public interface ISuppressKeyEventLogging + public interface ICanSuppressKeyEventLogging { + /// + /// Whether key event logging should be suppressed for this drawable. + /// + bool SuppressKeyEventLogging { get; } } } diff --git a/osu.Framework/Input/InputManager.cs b/osu.Framework/Input/InputManager.cs index 825bb5822..87efb4345 100644 --- a/osu.Framework/Input/InputManager.cs +++ b/osu.Framework/Input/InputManager.cs @@ -10,7 +10,6 @@ using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.ListExtensions; -using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -1002,37 +1001,13 @@ namespace osu.Framework.Input { foreach (var d in drawables) { - if (!d.TriggerEvent(e)) continue; - - if (shouldLog(e)) - { - string detail = d is ISuppressKeyEventLogging ? e.GetType().ReadableName() : e.ToString(); - Logger.Log($"{detail} handled by {d}.", LoggingTarget.Runtime, LogLevel.Debug); - } - - return true; + if (d.TriggerEvent(e)) + return true; } return false; } - private bool shouldLog(UIEvent eventType) - { - switch (eventType) - { - case KeyDownEvent k: - return !k.Repeat; - - case DragEvent: - case ScrollEvent: - case MouseMoveEvent: - return false; - - default: - return true; - } - } - /// /// Unfocus the current focused drawable if it is no longer in a valid state. /// diff --git a/osu.Framework/Input/KeyEventManager.cs b/osu.Framework/Input/KeyEventManager.cs index b716e7b67..d1434a3bf 100644 --- a/osu.Framework/Input/KeyEventManager.cs +++ b/osu.Framework/Input/KeyEventManager.cs @@ -33,5 +33,7 @@ namespace osu.Framework.Input protected override void HandleButtonUp(InputState state, List targets) => PropagateButtonEvent(targets, new KeyUpEvent(state, Button)); + + protected override bool SuppressLoggingEventInformation(Drawable drawable) => drawable is ICanSuppressKeyEventLogging canSuppress && canSuppress.SuppressKeyEventLogging; } } diff --git a/osu.Framework/Input/SDLWindowTextInput.cs b/osu.Framework/Input/SDLWindowTextInput.cs index c0fd539f4..79fa1d848 100644 --- a/osu.Framework/Input/SDLWindowTextInput.cs +++ b/osu.Framework/Input/SDLWindowTextInput.cs @@ -37,16 +37,16 @@ namespace osu.Framework.Input TriggerImeComposition(text, selectionStart, selectionLength); } - protected override void ActivateTextInput(bool allowIme) + protected override void ActivateTextInput(TextInputProperties properties) { window.TextInput += handleTextInput; window.TextEditing += handleTextEditing; - window.StartTextInput(allowIme); + window.StartTextInput(properties); } - protected override void EnsureTextInputActivated(bool allowIme) + protected override void EnsureTextInputActivated(TextInputProperties properties) { - window.StartTextInput(allowIme); + window.StartTextInput(properties); } protected override void DeactivateTextInput() diff --git a/osu.Framework/Input/TextInputProperties.cs b/osu.Framework/Input/TextInputProperties.cs new file mode 100644 index 000000000..1175e4141 --- /dev/null +++ b/osu.Framework/Input/TextInputProperties.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. + +namespace osu.Framework.Input +{ + /// + /// Represents a number of properties to consider during a text input session. + /// + /// The type of text being input. + /// + /// + /// Whether IME should be allowed during this text input session, if supported by the given text input type. + /// + /// + /// Note that this is just a hint to the native implementation, some might respect this, + /// while others will ignore and always have the IME (dis)allowed. + /// + /// + /// Whether text should be automatically capitalised. + public record struct TextInputProperties(TextInputType Type, bool AllowIme = true, bool AutoCapitalisation = false); +} diff --git a/osu.Framework/Input/TextInputSource.cs b/osu.Framework/Input/TextInputSource.cs index 237d17f36..9b77568e3 100644 --- a/osu.Framework/Input/TextInputSource.cs +++ b/osu.Framework/Input/TextInputSource.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using osu.Framework.Graphics.Primitives; @@ -29,7 +27,7 @@ namespace osu.Framework.Input /// Activates this . /// User text input can be acquired through , and . /// - /// Whether input using IME should be allowed. + /// A set of properties to consider during this text input session. /// /// Rough location of where the text will be input, so the native implementation /// can adjust virtual keyboards and IME popups. @@ -37,36 +35,36 @@ namespace osu.Framework.Input /// /// Each must be followed by a . /// - public void Activate(bool allowIme, RectangleF imeRectangle) + public void Activate(TextInputProperties properties, RectangleF imeRectangle) { if (Interlocked.Increment(ref activationCounter) == 1) { SetImeRectangle(imeRectangle); - ActivateTextInput(allowIme); + ActivateTextInput(properties); } else // the latest consumer that activated should always take precedence in (dis)allowing IME. - EnsureActivated(allowIme, imeRectangle); + EnsureActivated(properties, imeRectangle); } /// /// Ensures that the native implementation that retrieves user text input is activated /// and that the user can start entering text. /// - /// Whether input using IME should be allowed. + /// A set of properties to consider during this text input session. /// /// Rough location of where the text will be input, so the native implementation /// can adjust virtual keyboards and IME popups. Can be null to avoid changing /// the IME rectangle. /// - public void EnsureActivated(bool allowIme, RectangleF? imeRectangle = null) + public void EnsureActivated(TextInputProperties properties, RectangleF? imeRectangle = null) { if (activationCounter >= 1) { if (imeRectangle.HasValue) SetImeRectangle(imeRectangle.Value); - EnsureTextInputActivated(allowIme); + EnsureTextInputActivated(properties); } } @@ -103,29 +101,29 @@ namespace osu.Framework.Input /// /// Invoked on text input. /// - public event Action OnTextInput; + public event Action? OnTextInput; /// /// Invoked when IME composition starts or changes. /// /// Empty string for text means that the composition has been cancelled. - public event ImeCompositionDelegate OnImeComposition; + public event ImeCompositionDelegate? OnImeComposition; /// /// Invoked when IME composition successfully completes. /// - public event Action OnImeResult; + public event Action? OnImeResult; /// /// Activates the native implementation that provides text input. /// Should be overriden per-platform. /// - /// Whether input using IME should be allowed. + /// A set of properties to consider during this text input session. /// /// An active native implementation should call on new text input /// and forward IME composition events through and . /// - protected virtual void ActivateTextInput(bool allowIme) + protected virtual void ActivateTextInput(TextInputProperties properties) { } @@ -134,7 +132,7 @@ namespace osu.Framework.Input /// /// Only called if the native implementation has been activated with . /// - protected virtual void EnsureTextInputActivated(bool allowIme) + protected virtual void EnsureTextInputActivated(TextInputProperties properties) { } diff --git a/osu.Framework/Input/TextInputType.cs b/osu.Framework/Input/TextInputType.cs new file mode 100644 index 000000000..2a1645875 --- /dev/null +++ b/osu.Framework/Input/TextInputType.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Input +{ + public enum TextInputType + { + /// + /// Plain text, default type of text input. + /// + Text, + + /// + /// The text input is a person's name. + /// + Name, + + /// + /// The text input is an email address. + /// + EmailAddress, + + /// + /// The text input is a username. + /// + Username, + + /// + /// The text input is numerical. + /// + Number, + + /// + /// The text input is a password hidden from the user. + /// + Password, + + /// + /// The text input is a numerical password hidden from the user. + /// + NumericalPassword, + } + + public static class TextInputTypeExtensions + { + public static bool IsPassword(this TextInputType type) + { + switch (type) + { + case TextInputType.Password: + case TextInputType.NumericalPassword: + return true; + + default: + return false; + } + } + + public static bool IsNumerical(this TextInputType type) + { + switch (type) + { + case TextInputType.Number: + case TextInputType.NumericalPassword: + return true; + + default: + return false; + } + } + + public static bool SupportsIme(this TextInputType type) => type == TextInputType.Name || type == TextInputType.Text; + } +} diff --git a/osu.Framework/Platform/Apple/AppleTextureLoaderStore.cs b/osu.Framework/Platform/Apple/AppleTextureLoaderStore.cs new file mode 100644 index 000000000..4359b9290 --- /dev/null +++ b/osu.Framework/Platform/Apple/AppleTextureLoaderStore.cs @@ -0,0 +1,69 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Platform.Apple.Native; +using osu.Framework.Platform.Apple.Native.Accelerate; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace osu.Framework.Platform.Apple +{ + internal abstract class AppleTextureLoaderStore : TextureLoaderStore + { + protected AppleTextureLoaderStore(IResourceStore store) + : base(store) + { + } + + protected unsafe Image ImageFromCGImage(CGImage cgImage) + where TPixel : unmanaged, IPixel + { + int width = (int)cgImage.Width; + int height = (int)cgImage.Height; + + var format = new vImage_CGImageFormat + { + BitsPerComponent = 8, + BitsPerPixel = 32, + ColorSpace = CGColorSpace.CreateDeviceRGB(), + // notably, macOS & iOS generally use premultiplied alpha when rendering image to pixels via CGBitmapContext or otherwise, + // but vImage offers rendering as straight alpha by specifying Last instead of PremultipliedLast. + BitmapInfo = (CGBitmapInfo)CGImageAlphaInfo.Last, + Decode = null, + RenderingIntent = CGColorRenderingIntent.Default, + }; + + vImage_Buffer accImage = default; + + // perform initial call to retrieve preferred alignment and bytes-per-row values for the given image dimensions. + nuint alignment = (nuint)vImage.Init(&accImage, (uint)height, (uint)width, 32, vImage_Flags.NoAllocate); + Debug.Assert(alignment > 0); + + // allocate aligned memory region to contain image pixel data. + nuint bytesPerRow = accImage.BytesPerRow; + nuint bytesCount = bytesPerRow * accImage.Height; + accImage.Data = (byte*)NativeMemory.AlignedAlloc(bytesCount, alignment); + + var result = vImage.InitWithCGImage(&accImage, &format, null, cgImage.Handle, vImage_Flags.NoAllocate); + Debug.Assert(result == vImage_Error.NoError); + + var image = new Image(width, height); + + for (int i = 0; i < height; i++) + { + var imageRow = image.DangerousGetPixelRowMemory(i); + var dataRow = new ReadOnlySpan(&accImage.Data[(int)bytesPerRow * i], width); + dataRow.CopyTo(imageRow.Span); + } + + NativeMemory.AlignedFree(accImage.Data); + return image; + } + } +} diff --git a/osu.Framework/Platform/Apple/Native/Accelerate/vImage.cs b/osu.Framework/Platform/Apple/Native/Accelerate/vImage.cs new file mode 100644 index 000000000..60f677221 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/Accelerate/vImage.cs @@ -0,0 +1,19 @@ +// 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.Runtime.InteropServices; + +// ReSharper disable InconsistentNaming + +namespace osu.Framework.Platform.Apple.Native.Accelerate +{ + internal static unsafe partial class vImage + { + [LibraryImport(Interop.LIB_ACCELERATE, EntryPoint = "vImageBuffer_Init")] + internal static partial vImage_Error Init(vImage_Buffer* buf, uint height, uint width, uint pixelBits, vImage_Flags flags); + + [LibraryImport(Interop.LIB_ACCELERATE, EntryPoint = "vImageBuffer_InitWithCGImage")] + internal static partial vImage_Error InitWithCGImage(vImage_Buffer* buf, vImage_CGImageFormat* format, double* backgroundColour, IntPtr image, vImage_Flags flags); + } +} diff --git a/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Buffer.cs b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Buffer.cs new file mode 100644 index 000000000..100cec0b0 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Buffer.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +// ReSharper disable InconsistentNaming + +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native.Accelerate +{ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct vImage_Buffer + { + public byte* Data; + public nuint Height; + public nuint Width; + public nuint BytesPerRow; + } +} diff --git a/osu.Framework/Platform/Apple/Native/Accelerate/vImage_CGImageFormat.cs b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_CGImageFormat.cs new file mode 100644 index 000000000..87800f3f6 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_CGImageFormat.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. + +// ReSharper disable InconsistentNaming + +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native.Accelerate +{ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct vImage_CGImageFormat + { + public uint BitsPerComponent; + public uint BitsPerPixel; + public CGColorSpace ColorSpace; + public CGBitmapInfo BitmapInfo; + public uint Version; + public double* Decode; + public CGColorRenderingIntent RenderingIntent; + } +} diff --git a/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Error.cs b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Error.cs new file mode 100644 index 000000000..03366b12b --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Error.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +// ReSharper disable InconsistentNaming + +namespace osu.Framework.Platform.Apple.Native.Accelerate +{ + internal enum vImage_Error : long + { + OutOfPlaceOperationRequired = -21780, + ColorSyncIsAbsent = -21779, + InvalidImageFormat = -21778, + InvalidRowBytes = -21777, + InternalError = -21776, + UnknownFlagsBit = -21775, + BufferSizeMismatch = -21774, + InvalidParameter = -21773, + NullPointerArgument = -21772, + MemoryAllocationError = -21771, + InvalidOffsetY = -21770, + InvalidOffsetX = -21769, + InvalidEdgeStyle = -21768, + InvalidKernelSize = -21767, + RoiLargerThanInputBuffer = -21766, + NoError = 0, + } +} diff --git a/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Flags.cs b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Flags.cs new file mode 100644 index 000000000..8975dd8e1 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/Accelerate/vImage_Flags.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +// ReSharper disable InconsistentNaming + +namespace osu.Framework.Platform.Apple.Native.Accelerate +{ + internal enum vImage_Flags : uint + { + NoFlags = 0, + LeaveAlphaUnchanged = 1, + CopyInPlace = 2, + BackgroundColorFill = 4, + EdgeExtend = 8, + DoNotTile = 16, + HighQualityResampling = 32, + TruncateKernel = 64, + GetTempBufferSize = 128, + PrintDiagnosticsToConsole = 256, + NoAllocate = 512, + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGBitmapContext.cs b/osu.Framework/Platform/Apple/Native/CGBitmapContext.cs new file mode 100644 index 000000000..909761731 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGBitmapContext.cs @@ -0,0 +1,24 @@ +// 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.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + internal readonly partial struct CGBitmapContext + { + internal IntPtr Handle { get; } + + internal CGBitmapContext(IntPtr handle) + { + Handle = handle; + } + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGBitmapContextCreate")] + public static partial CGBitmapContext Create(IntPtr data, nuint width, nuint height, nuint bitsPerComponent, nuint bytesPerRow, CGColorSpace colorSpace, CGBitmapInfo bitmapInfo); + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGContextDrawImage")] + public static partial void DrawImage(CGBitmapContext context, CGRect rect, CGImage image); + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGBitmapInfo.cs b/osu.Framework/Platform/Apple/Native/CGBitmapInfo.cs new file mode 100644 index 000000000..27601ef7e --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGBitmapInfo.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Platform.Apple.Native +{ + public enum CGBitmapInfo : uint + { + None, + PremultipliedLast, + PremultipliedFirst, + Last, + First, + NoneSkipLast, + NoneSkipFirst, + Only, + AlphaInfoMask = 31, + FloatInfoMask = 3840, + FloatComponents = 256, + ByteOrderMask = 28672, + ByteOrderDefault = 0, + ByteOrder16Little = 4096, + ByteOrder32Little = 8192, + ByteOrder16Big = 12288, + ByteOrder32Big = 16384, + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGColorRenderingIntent.cs b/osu.Framework/Platform/Apple/Native/CGColorRenderingIntent.cs new file mode 100644 index 000000000..c9f8d8f62 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGColorRenderingIntent.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Platform.Apple.Native +{ + public enum CGColorRenderingIntent + { + Default, + AbsoluteColorimetric, + RelativeColorimetric, + Perceptual, + Saturation, + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGColorSpace.cs b/osu.Framework/Platform/Apple/Native/CGColorSpace.cs new file mode 100644 index 000000000..550ff1246 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGColorSpace.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 System; +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + public readonly partial struct CGColorSpace + { + internal IntPtr Handle { get; } + + internal CGColorSpace(IntPtr handle) + { + Handle = handle; + } + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGColorSpaceCreateDeviceRGB")] + internal static partial CGColorSpace CreateDeviceRGB(); + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGImage.cs b/osu.Framework/Platform/Apple/Native/CGImage.cs new file mode 100644 index 000000000..e0780e4db --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGImage.cs @@ -0,0 +1,33 @@ +// 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.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + internal readonly partial struct CGImage + { + internal IntPtr Handle { get; } + + public CGImage(IntPtr handle) + { + Handle = handle; + } + + internal nuint Width => GetWidth(this); + internal nuint Height => GetHeight(this); + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGImageGetWidth")] + internal static partial nuint GetWidth(CGImage image); + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGImageGetHeight")] + internal static partial nuint GetHeight(CGImage image); + + [LibraryImport(Interop.LIB_CORE_GRAPHICS, EntryPoint = "CGImageRelease")] + internal static partial void Release(CGImage image); + + [LibraryImport(Interop.LIB_CORE_FOUNDATION, EntryPoint = "CFGetRetainCount")] + internal static partial int GetRetainCount(CGImage image); + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGImageAlphaInfo.cs b/osu.Framework/Platform/Apple/Native/CGImageAlphaInfo.cs new file mode 100644 index 000000000..ec8f6beba --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGImageAlphaInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Platform.Apple.Native +{ + internal enum CGImageAlphaInfo : uint + { + None, + PremultipliedLast, + PremultipliedFirst, + Last, + First, + NoneSkipLast, + NoneSkipFirst, + Only, + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGPoint.cs b/osu.Framework/Platform/Apple/Native/CGPoint.cs new file mode 100644 index 000000000..ded075d71 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGPoint.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + [StructLayout(LayoutKind.Sequential)] + internal struct CGPoint + { + internal double X; + internal double Y; + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGRect.cs b/osu.Framework/Platform/Apple/Native/CGRect.cs new file mode 100644 index 000000000..310621c7a --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGRect.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + [StructLayout(LayoutKind.Sequential)] + internal struct CGRect + { + internal CGPoint Origin; + internal CGSize Size; + } +} diff --git a/osu.Framework/Platform/Apple/Native/CGSize.cs b/osu.Framework/Platform/Apple/Native/CGSize.cs new file mode 100644 index 000000000..f6e89d7ab --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/CGSize.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Apple.Native +{ + [StructLayout(LayoutKind.Sequential)] + internal struct CGSize + { + internal double Width; + internal double Height; + } +} diff --git a/osu.Framework/Platform/Apple/Native/Class.cs b/osu.Framework/Platform/Apple/Native/Class.cs index dda3c7669..62cae2959 100644 --- a/osu.Framework/Platform/Apple/Native/Class.cs +++ b/osu.Framework/Platform/Apple/Native/Class.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.InteropServices; -using osu.Framework.Platform.MacOS.Native; namespace osu.Framework.Platform.Apple.Native { diff --git a/osu.Framework/Platform/Apple/Native/Interop.cs b/osu.Framework/Platform/Apple/Native/Interop.cs index c97725dc9..da4d464bc 100644 --- a/osu.Framework/Platform/Apple/Native/Interop.cs +++ b/osu.Framework/Platform/Apple/Native/Interop.cs @@ -13,6 +13,7 @@ namespace osu.Framework.Platform.Apple.Native internal const string LIB_OBJ_C = "/usr/lib/libobjc.dylib"; internal const string LIB_CORE_GRAPHICS = "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"; internal const string LIB_ACCELERATE = "/System/Library/Frameworks/Accelerate.framework/Accelerate"; + internal const string LIB_CORE_FOUNDATION = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; internal const int RTLD_NOW = 2; diff --git a/osu.Framework/Platform/Apple/Native/NSArray.cs b/osu.Framework/Platform/Apple/Native/NSArray.cs index 998734096..3b3f0af26 100644 --- a/osu.Framework/Platform/Apple/Native/NSArray.cs +++ b/osu.Framework/Platform/Apple/Native/NSArray.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Platform.MacOS.Native; namespace osu.Framework.Platform.Apple.Native { diff --git a/osu.Framework/Platform/Apple/Native/NSAutoreleasePool.cs b/osu.Framework/Platform/Apple/Native/NSAutoreleasePool.cs new file mode 100644 index 000000000..736d0db24 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/NSAutoreleasePool.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Platform.Apple.Native +{ + internal readonly struct NSAutoreleasePool : IDisposable + { + internal IntPtr Handle { get; } + + internal NSAutoreleasePool(IntPtr handle) + { + Handle = handle; + } + + private static readonly IntPtr class_pointer = Class.Get("NSAutoreleasePool"); + private static readonly IntPtr sel_alloc = Selector.Get("alloc"); + private static readonly IntPtr sel_init = Selector.Get("init"); + private static readonly IntPtr sel_drain = Selector.Get("drain"); + + public static NSAutoreleasePool Init() + { + var pool = alloc(); + Interop.SendIntPtr(pool.Handle, sel_init); + return pool; + } + + private static NSAutoreleasePool alloc() => new NSAutoreleasePool(Interop.SendIntPtr(class_pointer, sel_alloc)); + + public void Dispose() + { + if (Handle != IntPtr.Zero) + Interop.SendIntPtr(Handle, sel_drain); + } + } +} diff --git a/osu.Framework/Platform/Apple/Native/NSData.cs b/osu.Framework/Platform/Apple/Native/NSData.cs index 808c29f6d..dc7c18d07 100644 --- a/osu.Framework/Platform/Apple/Native/NSData.cs +++ b/osu.Framework/Platform/Apple/Native/NSData.cs @@ -3,16 +3,14 @@ using System; using System.Runtime.InteropServices; -using osu.Framework.Platform.MacOS.Native; namespace osu.Framework.Platform.Apple.Native { - internal readonly struct NSData : IDisposable + internal readonly struct NSData { internal IntPtr Handle { get; } private static readonly IntPtr class_pointer = Class.Get("NSData"); - private static readonly IntPtr sel_release = Selector.Get("release"); private static readonly IntPtr sel_data_with_bytes = Selector.Get("dataWithBytes:length:"); private static readonly IntPtr sel_bytes = Selector.Get("bytes"); private static readonly IntPtr sel_length = Selector.Get("length"); @@ -22,6 +20,8 @@ namespace osu.Framework.Platform.Apple.Native Handle = handle; } + public static implicit operator NSData(NSMutableData data) => new NSData(data.Handle); + internal byte[] ToBytes() { IntPtr pointer = Interop.SendIntPtr(Handle, sel_bytes); @@ -32,14 +32,6 @@ namespace osu.Framework.Platform.Apple.Native return bytes; } - internal void Release() => Interop.SendVoid(Handle, sel_release); - - public void Dispose() - { - if (Handle != IntPtr.Zero) - Release(); - } - internal static unsafe NSData FromBytes(ReadOnlySpan bytes) { fixed (byte* ptr = bytes) diff --git a/osu.Framework/Platform/Apple/Native/NSMutableData.cs b/osu.Framework/Platform/Apple/Native/NSMutableData.cs new file mode 100644 index 000000000..dd1eed658 --- /dev/null +++ b/osu.Framework/Platform/Apple/Native/NSMutableData.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Platform.Apple.Native +{ + internal readonly struct NSMutableData + { + internal IntPtr Handle { get; } + + private static readonly IntPtr class_pointer = Class.Get("NSMutableData"); + private static readonly IntPtr sel_data_with_length = Selector.Get("dataWithLength:"); + private static readonly IntPtr sel_mutable_bytes = Selector.Get("mutableBytes"); + + internal NSMutableData(IntPtr handle) + { + Handle = handle; + } + + internal unsafe byte* MutableBytes => (byte*)Interop.SendIntPtr(Handle, sel_mutable_bytes); + + internal static NSMutableData FromLength(int length) + { + IntPtr handle = Interop.SendIntPtr(class_pointer, sel_data_with_length, length); + return new NSMutableData(handle); + } + } +} diff --git a/osu.Framework/Platform/Apple/Native/NSNotificationCenter.cs b/osu.Framework/Platform/Apple/Native/NSNotificationCenter.cs index 3459e8015..6d8ff30bc 100644 --- a/osu.Framework/Platform/Apple/Native/NSNotificationCenter.cs +++ b/osu.Framework/Platform/Apple/Native/NSNotificationCenter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Platform.MacOS.Native; namespace osu.Framework.Platform.Apple.Native { diff --git a/osu.Framework/Platform/Apple/Native/NSString.cs b/osu.Framework/Platform/Apple/Native/NSString.cs index e1c3ce459..902ba764d 100644 --- a/osu.Framework/Platform/Apple/Native/NSString.cs +++ b/osu.Framework/Platform/Apple/Native/NSString.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.InteropServices; -using osu.Framework.Platform.MacOS.Native; namespace osu.Framework.Platform.Apple.Native { diff --git a/osu.Framework/Platform/MacOS/Native/Selector.cs b/osu.Framework/Platform/Apple/Native/Selector.cs similarity index 83% rename from osu.Framework/Platform/MacOS/Native/Selector.cs rename to osu.Framework/Platform/Apple/Native/Selector.cs index e54a69bd2..3d5805c1f 100644 --- a/osu.Framework/Platform/MacOS/Native/Selector.cs +++ b/osu.Framework/Platform/Apple/Native/Selector.cs @@ -3,9 +3,8 @@ using System; using System.Runtime.InteropServices; -using osu.Framework.Platform.Apple.Native; -namespace osu.Framework.Platform.MacOS.Native +namespace osu.Framework.Platform.Apple.Native { internal static partial class Selector { diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index 85b83b988..3d159dc63 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -741,6 +741,7 @@ namespace osu.Framework.Platform CacheStorage = GetDefaultGameStorage().GetStorageForDirectory("cache"); SetupForRun(); + game.SetupLogging(Storage, CacheStorage); populateInputHandlers(); diff --git a/osu.Framework/Platform/ISDLWindow.cs b/osu.Framework/Platform/ISDLWindow.cs index 410fe82dc..fb9339a00 100644 --- a/osu.Framework/Platform/ISDLWindow.cs +++ b/osu.Framework/Platform/ISDLWindow.cs @@ -36,7 +36,7 @@ namespace osu.Framework.Platform void UpdateMousePosition(Vector2 position); - void StartTextInput(bool allowIme); + void StartTextInput(TextInputProperties properties); void StopTextInput(); void SetTextInputRect(RectangleF rectangle); void ResetIme(); diff --git a/osu.Framework/Platform/MacOS/MacOSClipboard.cs b/osu.Framework/Platform/MacOS/MacOSClipboard.cs index 4d6e29249..86ecbe2ee 100644 --- a/osu.Framework/Platform/MacOS/MacOSClipboard.cs +++ b/osu.Framework/Platform/MacOS/MacOSClipboard.cs @@ -35,9 +35,12 @@ namespace osu.Framework.Platform.MacOS using var stream = new MemoryStream(); image.SaveAsTiff(stream); - using var nsData = NSData.FromBytes(stream.ToArray()); - using var nsImage = NSImage.LoadFromData(nsData); - return setToPasteboard(nsImage.Handle); + using (NSAutoreleasePool.Init()) + { + var nsData = NSData.FromBytes(stream.ToArray()); + using var nsImage = NSImage.LoadFromData(nsData); + return setToPasteboard(nsImage.Handle); + } } private IntPtr getFromPasteboard(IntPtr @class) diff --git a/osu.Framework/Platform/MacOS/MacOSGameHost.cs b/osu.Framework/Platform/MacOS/MacOSGameHost.cs index 67100d46b..234976426 100644 --- a/osu.Framework/Platform/MacOS/MacOSGameHost.cs +++ b/osu.Framework/Platform/MacOS/MacOSGameHost.cs @@ -7,10 +7,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Handlers; using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform.MacOS.Native; @@ -44,6 +46,9 @@ namespace osu.Framework.Platform.MacOS protected override ReadableKeyCombinationProvider CreateReadableKeyCombinationProvider() => new MacOSReadableKeyCombinationProvider(); + public override IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) + => new MacOSTextureLoaderStore(underlyingStore); + protected override void Swap() { base.Swap(); diff --git a/osu.Framework/Platform/MacOS/MacOSTextureLoaderStore.cs b/osu.Framework/Platform/MacOS/MacOSTextureLoaderStore.cs new file mode 100644 index 000000000..3e9a38a46 --- /dev/null +++ b/osu.Framework/Platform/MacOS/MacOSTextureLoaderStore.cs @@ -0,0 +1,40 @@ +// 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.IO; +using osu.Framework.IO.Stores; +using osu.Framework.Platform.Apple; +using osu.Framework.Platform.Apple.Native; +using osu.Framework.Platform.MacOS.Native; +using SixLabors.ImageSharp; + +namespace osu.Framework.Platform.MacOS +{ + internal class MacOSTextureLoaderStore : AppleTextureLoaderStore + { + public MacOSTextureLoaderStore(IResourceStore store) + : base(store) + { + } + + protected override unsafe Image ImageFromStream(Stream stream) + { + using (NSAutoreleasePool.Init()) + { + int length = (int)(stream.Length - stream.Position); + var nativeData = NSMutableData.FromLength(length); + + var bytesSpan = new Span(nativeData.MutableBytes, length); + stream.ReadExactly(bytesSpan); + + using var nsImage = NSImage.LoadFromData(nativeData); + if (nsImage.Handle == IntPtr.Zero) + throw new ArgumentException($"{nameof(Image)} could not be created from {nameof(stream)}."); + + var cgImage = nsImage.CGImage; + return ImageFromCGImage(cgImage); + } + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/NSImage.cs b/osu.Framework/Platform/MacOS/Native/NSImage.cs index 6ee354f6b..1a87d09f1 100644 --- a/osu.Framework/Platform/MacOS/Native/NSImage.cs +++ b/osu.Framework/Platform/MacOS/Native/NSImage.cs @@ -21,6 +21,9 @@ namespace osu.Framework.Platform.MacOS.Native private static readonly IntPtr sel_release = Selector.Get("release"); private static readonly IntPtr sel_init_with_data = Selector.Get("initWithData:"); private static readonly IntPtr sel_tiff_representation = Selector.Get("TIFFRepresentation"); + private static readonly IntPtr sel_cg_image_for_proposed_rect = Selector.Get("CGImageForProposedRect:context:hints:"); + + internal CGImage CGImage => new CGImage(Interop.SendIntPtr(Handle, sel_cg_image_for_proposed_rect, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)); internal NSData TiffRepresentation => new NSData(Interop.SendIntPtr(Handle, sel_tiff_representation)); diff --git a/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs b/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs index bb5e4350d..ea645b5ec 100644 --- a/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs +++ b/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Platform.Apple.Native; -using osu.Framework.Platform.MacOS.Native; using osu.Framework.Platform.SDL2; using osuTK; +using Selector = osu.Framework.Platform.Apple.Native.Selector; namespace osu.Framework.Platform.MacOS { diff --git a/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs b/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs index c9abb8ec9..92b18a428 100644 --- a/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs +++ b/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Platform.Apple.Native; -using osu.Framework.Platform.MacOS.Native; using osu.Framework.Platform.SDL3; using osuTK; +using Selector = osu.Framework.Platform.Apple.Native.Selector; namespace osu.Framework.Platform.MacOS { diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs index 43a62dd8b..5c0088392 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -172,7 +172,7 @@ namespace osu.Framework.Platform.SDL2 } } - public virtual void StartTextInput(bool allowIme) => ScheduleCommand(SDL_StartTextInput); + public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(SDL_StartTextInput); public void StopTextInput() => ScheduleCommand(SDL_StopTextInput); diff --git a/osu.Framework/Platform/SDL3/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs index e2b77fc67..5a3538d94 100644 --- a/osu.Framework/Platform/SDL3/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -1011,6 +1011,34 @@ namespace osu.Framework.Platform.SDL3 w = rectangle.Width, }; + public static SDL_TextInputType ToSDLTextInputType(this TextInputType type) + { + switch (type) + { + default: + case TextInputType.Text: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_TEXT; + + case TextInputType.Name: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_TEXT_NAME; + + case TextInputType.EmailAddress: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_TEXT_EMAIL; + + case TextInputType.Username: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_TEXT_USERNAME; + + case TextInputType.Number: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_NUMBER; + + case TextInputType.Password: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN; + + case TextInputType.NumericalPassword: + return SDL_TextInputType.SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN; + } + } + public static unsafe DisplayMode ToDisplayMode(this SDL_DisplayMode mode, int displayIndex) { int bpp; diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index 1e9af042b..91d782ef4 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -185,7 +185,22 @@ namespace osu.Framework.Platform.SDL3 } } - public virtual void StartTextInput(bool allowIme) => ScheduleCommand(() => SDL_StartTextInput(SDLWindowHandle)); + private SDL_PropertiesID? currentTextInputProperties; + + public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() => + { + currentTextInputProperties ??= SDL_CreateProperties(); + + var props = currentTextInputProperties.Value; + SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType()); + + if (!properties.AutoCapitalisation) + SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE); + else + SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER); + + SDL_StartTextInputWithProperties(SDLWindowHandle, props); + }); public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle)); @@ -196,7 +211,11 @@ namespace osu.Framework.Platform.SDL3 public virtual void ResetIme() => ScheduleCommand(() => { SDL_StopTextInput(SDLWindowHandle); - SDL_StartTextInput(SDLWindowHandle); + + if (currentTextInputProperties is SDL_PropertiesID props) + SDL_StartTextInputWithProperties(SDLWindowHandle, props); + else + SDL_StartTextInput(SDLWindowHandle); }); public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() => diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index 17c6a8df5..21e644191 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using osu.Framework.Input; using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Platform.SDL2; using osu.Framework.Platform.Windows.Native; @@ -138,10 +139,10 @@ namespace osu.Framework.Platform.Windows #region IME handling - public override void StartTextInput(bool allowIme) + public override void StartTextInput(TextInputProperties properties) { - base.StartTextInput(allowIme); - ScheduleCommand(() => Imm.SetImeAllowed(WindowHandle, allowIme)); + base.StartTextInput(properties); + ScheduleCommand(() => Imm.SetImeAllowed(WindowHandle, properties.Type.SupportsIme() && properties.AllowIme)); } public override void ResetIme() => ScheduleCommand(() => Imm.CancelComposition(WindowHandle)); diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index d04ea28ba..5147a787a 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -5,6 +5,7 @@ using System; using System.Drawing; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using osu.Framework.Input; using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Platform.SDL3; using osu.Framework.Platform.Windows.Native; @@ -92,10 +93,10 @@ namespace osu.Framework.Platform.Windows } } - public override void StartTextInput(bool allowIme) + public override void StartTextInput(TextInputProperties properties) { - base.StartTextInput(allowIme); - ScheduleCommand(() => Imm.SetImeAllowed(WindowHandle, allowIme)); + base.StartTextInput(properties); + ScheduleCommand(() => Imm.SetImeAllowed(WindowHandle, properties.Type.SupportsIme() && properties.AllowIme)); } public override void ResetIme() => ScheduleCommand(() => Imm.CancelComposition(WindowHandle)); diff --git a/osu.Framework/Testing/Input/ManualTextInputSource.cs b/osu.Framework/Testing/Input/ManualTextInputSource.cs index def56cb23..94b722da5 100644 --- a/osu.Framework/Testing/Input/ManualTextInputSource.cs +++ b/osu.Framework/Testing/Input/ManualTextInputSource.cs @@ -8,8 +8,8 @@ namespace osu.Framework.Testing.Input { public class ManualTextInputSource : TextInputSource { - public readonly Queue ActivationQueue = new Queue(); - public readonly Queue EnsureActivatedQueue = new Queue(); + public readonly Queue ActivationQueue = new Queue(); + public readonly Queue EnsureActivatedQueue = new Queue(); public readonly Queue DeactivationQueue = new Queue(); public void Text(string text) => TriggerTextInput(text); @@ -32,16 +32,16 @@ namespace osu.Framework.Testing.Input base.TriggerImeComposition(string.Empty, 0, 0); } - protected override void ActivateTextInput(bool allowIme) + protected override void ActivateTextInput(TextInputProperties properties) { - base.ActivateTextInput(allowIme); - ActivationQueue.Enqueue(allowIme); + base.ActivateTextInput(properties); + ActivationQueue.Enqueue(properties); } - protected override void EnsureTextInputActivated(bool allowIme) + protected override void EnsureTextInputActivated(TextInputProperties properties) { - base.EnsureTextInputActivated(allowIme); - EnsureActivatedQueue.Enqueue(allowIme); + base.EnsureTextInputActivated(properties); + EnsureActivatedQueue.Enqueue(properties); } protected override void DeactivateTextInput() diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 32f6ff813..fd2172ef0 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -38,7 +38,7 @@ - +