Merge branch 'master' into better-SDL-pen-handling

This commit is contained in:
Salman Alshamrani
2025-01-21 04:36:32 -05:00
67 changed files with 1097 additions and 272 deletions

View File

@@ -15,7 +15,7 @@
]
},
"nvika": {
"version": "3.0.0",
"version": "4.0.0",
"commands": [
"nvika"
]

View File

@@ -342,6 +342,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ARGB/@EntryIndexedValue">ARGB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BPM/@EntryIndexedValue">BPM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CG/@EntryIndexedValue">CG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FBO/@EntryIndexedValue">FBO</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CCL/@EntryIndexedValue">CCL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue">GC</s:String>

View File

@@ -18,7 +18,6 @@
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.SDL3-CS.Android" Version="2024.1128.0" />
<PackageReference Include="Xamarin.AndroidX.Window" Version="1.2.0.1" PrivateAssets="compile" />
</ItemGroup>
</Project>

View File

@@ -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));

View File

@@ -0,0 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Drawable> 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<BoxWithDouble>().Skip(index).First()));
AddUntilStep($"{index} is visible", () => !scrollContainer.ChildrenOfType<BoxWithDouble>().Skip(index).First().IsMaskedAway);
}
public partial class DoubleScrollContainer : BasicScrollContainer
{
private readonly Container<BoxWithDouble> 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<BoxWithDouble>
{
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; }
}
}
}

View File

@@ -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", () =>

View File

@@ -467,7 +467,7 @@ namespace osu.Framework.Tests.Visual.UserInterface
private partial class TestRearrangeableList : BasicRearrangeableListContainer<int>
{
public float ScrollPosition => ScrollContainer.Current;
public float ScrollPosition => (float)ScrollContainer.Current;
public new IReadOnlyDictionary<int, RearrangeableListItem<int>> ItemMap => base.ItemMap;

View File

@@ -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);

View File

@@ -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<bool> InputErrorQueue = new Queue<bool>();
public readonly Queue<string> UserConsumedTextQueue = new Queue<string>();
public readonly Queue<string> UserRemovedTextQueue = new Queue<string>();

View File

@@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TextInputType>())
{
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
},
});
}
});
}
}
}

View File

@@ -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<byte[]> store)
: base(store)
@@ -26,81 +20,21 @@ namespace osu.Framework.iOS.Graphics.Textures
protected override unsafe Image<TPixel> ImageFromStream<TPixel>(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<byte>(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<TPixel>(width, height);
byte* data = (byte*)accelerateImage.Data;
for (int i = 0; i < height; i++)
{
var imageRow = image.DangerousGetPixelRowMemory(i);
var dataRow = new ReadOnlySpan<TPixel>(&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<TPixel>(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
}
}

View File

@@ -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)

View File

@@ -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<InputHandler> CreateAvailableInputHandlers()
{
var handlers = base.CreateAvailableInputHandlers();
foreach (var h in handlers.OfType<MouseHandler>())
{
// 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;

View File

@@ -128,6 +128,20 @@ namespace osu.Framework
protected sealed override void AddInternal(Drawable drawable) => throw new InvalidOperationException($"Use {nameof(Add)} or {nameof(Content)} instead.");
/// <summary>
/// The earliest point of entry during <see cref="GameHost.Run"/> starting execution of a game.
/// This should be used to set up any low level tasks such as exception handling.
/// </summary>
/// <remarks>
/// At this point in execution, only <see cref="GameHost.Storage"/> and <see cref="GameHost.CacheStorage"/> are guaranteed to be valid for use.
/// They are provided as <paramref name="gameStorage"/> and <paramref name="cacheStorage"/> respectively for convenience.
/// </remarks>
/// <param name="gameStorage">The default game storage.</param>
/// <param name="cacheStorage">The default cache storage.</param>
public virtual void SetupLogging(Storage gameStorage, Storage cacheStorage)
{
}
/// <summary>
/// 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.
/// </summary>

View File

@@ -118,7 +118,7 @@ namespace osu.Framework.Graphics.Containers
/// <summary>
/// The current scroll position.
/// </summary>
public float Current { get; private set; }
public double Current { get; private set; }
/// <summary>
/// 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
/// <remarks>
/// When not animating scroll position, this will always be equal to <see cref="Current"/>.
/// </remarks>
public float Target { get; private set; }
public double Target { get; private set; }
/// <summary>
/// The maximum distance that can be scrolled in the scroll direction.
/// </summary>
public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);
public double ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);
/// <summary>
/// The maximum distance that the scrollbar can move in the scroll direction.
@@ -139,14 +139,14 @@ namespace osu.Framework.Graphics.Containers
/// <remarks>
/// May not be accurate to actual display of scrollbar if <see cref="ToScrollbarPosition"/> or <see cref="FromScrollbarPosition"/> are overridden.
/// </remarks>
protected float ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0);
protected double ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0);
/// <summary>
/// Clamp a value to the available scroll range.
/// </summary>
/// <param name="position">The value to clamp.</param>
/// <param name="extension">An extension value beyond the normal extent.</param>
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<T> 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.
/// </summary>
/// <param name="offset">The scroll offset.</param>
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);
/// <summary>
@@ -454,7 +454,7 @@ namespace osu.Framework.Graphics.Containers
/// </summary>
/// <param name="offset">The amount by which we should scroll.</param>
/// <param name="animated">Whether to animate the movement.</param>
public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated);
public void ScrollBy(double offset, bool animated = true) => scrollTo(Target + offset, animated);
/// <summary>
/// Handle a scroll to an absolute position from a user input.
@@ -462,7 +462,7 @@ namespace osu.Framework.Graphics.Containers
/// <param name="value">The position to scroll to.</param>
/// <param name="animated">Whether to animate the movement.</param>
/// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
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);
/// <summary>
@@ -471,9 +471,9 @@ namespace osu.Framework.Graphics.Containers
/// <param name="value">The position to scroll to.</param>
/// <param name="animated">Whether to animate the movement.</param>
/// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
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
/// <param name="animated">Whether to animate the movement.</param>
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
/// <param name="d">The child to get the position from.</param>
/// <param name="offset">Positional offset in the child's space.</param>
/// <returns>The position of the child.</returns>
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];
/// <summary>
/// Determines the position of a child in the content.
/// </summary>
/// <param name="d">The child to get the position from.</param>
/// <returns>The position of the child.</returns>
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();
}
/// <summary>
/// This is the final internal step of updating the scroll container, which takes
/// <see cref="Current"/> and applies it to <see cref="ScrollContent"/> 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.
/// </summary>
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));
}
/// <summary>
@@ -594,12 +606,12 @@ namespace osu.Framework.Graphics.Containers
/// </summary>
/// <param name="scrollPosition">The absolute scroll position (e.g. <see cref="Current"/>).</param>
/// <returns>The scrollbar position.</returns>
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));
}
/// <summary>
@@ -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));
}
/// <summary>

View File

@@ -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);
}

View File

@@ -49,7 +49,7 @@ namespace osu.Framework.Graphics
/// <param name="drawable">The <see cref="Drawable"/> to be checked.</param>
/// <param name="root">The root to be checked against.</param>
/// <returns>Whether the drawable was rooted.</returns>
internal static bool IsRootedAt(this Drawable? drawable, Drawable root)
public static bool IsRootedAt(this Drawable? drawable, Drawable root)
{
if (drawable == root) return true;

View File

@@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}

View File

@@ -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()

View File

@@ -29,13 +29,18 @@ using osuTK.Input;
namespace osu.Framework.Graphics.UserInterface
{
public abstract partial class TextBox : TabbableContainer, IHasCurrentValue<string>, IKeyBindingHandler<PlatformAction>
public abstract partial class TextBox : TabbableContainer, IHasCurrentValue<string>, IKeyBindingHandler<PlatformAction>, ICanSuppressKeyEventLogging
{
protected FillFlowContainer TextFlow { get; private set; }
protected Container TextContainer { get; private set; }
public override bool HandleNonPositionalInput => HasFocus;
/// <summary>
/// A character displayed whenever the type of text input set by <see cref="TextInputProperties.Type"/> is hidden.
/// </summary>
protected virtual char MaskCharacter => '*';
/// <summary>
/// Padding to be used within the TextContainer. Requires special handling due to the sideways scrolling of text content.
/// </summary>
@@ -50,12 +55,14 @@ namespace osu.Framework.Graphics.UserInterface
/// <summary>
/// Whether clipboard copying functionality is allowed.
/// </summary>
protected virtual bool AllowClipboardExport => true;
protected virtual bool AllowClipboardExport => !InputProperties.Type.IsPassword();
/// <summary>
/// Whether seeking to word boundaries is allowed.
/// </summary>
protected virtual bool AllowWordNavigation => true;
protected virtual bool AllowWordNavigation => !InputProperties.Type.IsPassword();
bool ICanSuppressKeyEventLogging.SuppressKeyEventLogging => InputProperties.Type.IsPassword();
/// <summary>
/// Represents the left/right selection coordinates of the word double clicked on when dragging.
@@ -67,18 +74,14 @@ namespace osu.Framework.Graphics.UserInterface
/// </summary>
public virtual bool HandleLeftRightArrows => true;
/// <summary>
/// Whether to allow IME input when this text box has input focus.
/// </summary>
/// <remarks>
/// This is just a hint to the native implementation, some might respect this,
/// while others will ignore and always have the IME (dis)allowed.
/// </remarks>
/// <example>
/// Useful for situations where IME input is not wanted, such as for passwords, numbers, or romanised text.
/// </example>
[Obsolete($"Use {nameof(InputProperties)} instead.")] // can be removed 20250506
protected virtual bool AllowIme => true;
/// <summary>
/// A set of properties to consider when interacting with this <see cref="TextBox"/>.
/// </summary>
public TextInputProperties InputProperties { get; init; }
/// <summary>
/// Check if a character can be added to this TextBox.
/// </summary>
@@ -87,9 +90,14 @@ namespace osu.Framework.Graphics.UserInterface
protected virtual bool CanAddCharacter(char character) => true;
/// <summary>
/// Private helper for <see cref="CanAddCharacter"/>, additionally requiring that the character is not a control character.
/// Private helper for <see cref="CanAddCharacter"/>, additionally requiring that the character is not a control character and obeys <see cref="TextInputProperties.Type"/>.
/// </summary>
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<Drawable> charsRight = new List<Drawable>();
@@ -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;

View File

@@ -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;
}
/// <summary>
/// Whether information about the event should be suppressed from logging for the given drawable.
/// </summary>
protected virtual bool SuppressLoggingEventInformation(Drawable drawable) => false;
}
}

View File

@@ -4,10 +4,14 @@
namespace osu.Framework.Input
{
/// <summary>
/// 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.
/// </summary>
public interface ISuppressKeyEventLogging
public interface ICanSuppressKeyEventLogging
{
/// <summary>
/// Whether key event logging should be suppressed for this drawable.
/// </summary>
bool SuppressKeyEventLogging { get; }
}
}

View File

@@ -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;
}
}
/// <summary>
/// Unfocus the current focused drawable if it is no longer in a valid state.
/// </summary>

View File

@@ -33,5 +33,7 @@ namespace osu.Framework.Input
protected override void HandleButtonUp(InputState state, List<Drawable> targets) =>
PropagateButtonEvent(targets, new KeyUpEvent(state, Button));
protected override bool SuppressLoggingEventInformation(Drawable drawable) => drawable is ICanSuppressKeyEventLogging canSuppress && canSuppress.SuppressKeyEventLogging;
}
}

View File

@@ -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()

View File

@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Framework.Input
{
/// <summary>
/// Represents a number of properties to consider during a text input session.
/// </summary>
/// <param name="Type">The type of text being input.</param>
/// <param name="AllowIme">
/// <para>
/// Whether IME should be allowed during this text input session, if supported by the given text input type.
/// </para>
/// <para>
/// 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.
/// </para>
/// </param>
/// <param name="AutoCapitalisation">Whether text should be automatically capitalised.</param>
public record struct TextInputProperties(TextInputType Type, bool AllowIme = true, bool AutoCapitalisation = false);
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 <see cref="TextInputSource"/>.
/// User text input can be acquired through <see cref="OnTextInput"/>, <see cref="OnImeComposition"/> and <see cref="OnImeResult"/>.
/// </summary>
/// <param name="allowIme">Whether input using IME should be allowed.</param>
/// <param name="properties">A set of properties to consider during this text input session.</param>
/// <param name="imeRectangle">
/// 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
/// <remarks>
/// Each <see cref="Activate"/> must be followed by a <see cref="Deactivate"/>.
/// </remarks>
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);
}
/// <summary>
/// Ensures that the native implementation that retrieves user text input is activated
/// and that the user can start entering text.
/// </summary>
/// <param name="allowIme">Whether input using IME should be allowed.</param>
/// <param name="properties">A set of properties to consider during this text input session.</param>
/// <param name="imeRectangle">
/// Rough location of where the text will be input, so the native implementation
/// can adjust virtual keyboards and IME popups. Can be <c>null</c> to avoid changing
/// the IME rectangle.
/// </param>
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
/// <summary>
/// Invoked on text input.
/// </summary>
public event Action<string> OnTextInput;
public event Action<string>? OnTextInput;
/// <summary>
/// Invoked when IME composition starts or changes.
/// </summary>
/// <remarks>Empty string for text means that the composition has been cancelled.</remarks>
public event ImeCompositionDelegate OnImeComposition;
public event ImeCompositionDelegate? OnImeComposition;
/// <summary>
/// Invoked when IME composition successfully completes.
/// </summary>
public event Action<string> OnImeResult;
public event Action<string>? OnImeResult;
/// <summary>
/// Activates the native implementation that provides text input.
/// Should be overriden per-platform.
/// </summary>
/// <param name="allowIme">Whether input using IME should be allowed.</param>
/// <param name="properties">A set of properties to consider during this text input session.</param>
/// <remarks>
/// An active native implementation should call <see cref="TriggerTextInput"/> on new text input
/// and forward IME composition events through <see cref="TriggerImeComposition"/> and <see cref="TriggerImeResult"/>.
/// </remarks>
protected virtual void ActivateTextInput(bool allowIme)
protected virtual void ActivateTextInput(TextInputProperties properties)
{
}
@@ -134,7 +132,7 @@ namespace osu.Framework.Input
/// <remarks>
/// Only called if the native implementation has been activated with <see cref="Activate"/>.
/// </remarks>
protected virtual void EnsureTextInputActivated(bool allowIme)
protected virtual void EnsureTextInputActivated(TextInputProperties properties)
{
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Framework.Input
{
public enum TextInputType
{
/// <summary>
/// Plain text, default type of text input.
/// </summary>
Text,
/// <summary>
/// The text input is a person's name.
/// </summary>
Name,
/// <summary>
/// The text input is an email address.
/// </summary>
EmailAddress,
/// <summary>
/// The text input is a username.
/// </summary>
Username,
/// <summary>
/// The text input is numerical.
/// </summary>
Number,
/// <summary>
/// The text input is a password hidden from the user.
/// </summary>
Password,
/// <summary>
/// The text input is a numerical password hidden from the user.
/// </summary>
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;
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<byte[]> store)
: base(store)
{
}
protected unsafe Image<TPixel> ImageFromCGImage<TPixel>(CGImage cgImage)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(width, height);
for (int i = 0; i < height; i++)
{
var imageRow = image.DangerousGetPixelRowMemory(i);
var dataRow = new ReadOnlySpan<TPixel>(&accImage.Data[(int)bytesPerRow * i], width);
dataRow.CopyTo(imageRow.Span);
}
NativeMemory.AlignedFree(accImage.Data);
return image;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}

View File

@@ -3,7 +3,6 @@
using System;
using System.Runtime.InteropServices;
using osu.Framework.Platform.MacOS.Native;
namespace osu.Framework.Platform.Apple.Native
{

View File

@@ -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;

View File

@@ -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
{

View File

@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@@ -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<byte> bytes)
{
fixed (byte* ptr = bytes)

View File

@@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@@ -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
{

View File

@@ -3,7 +3,6 @@
using System;
using System.Runtime.InteropServices;
using osu.Framework.Platform.MacOS.Native;
namespace osu.Framework.Platform.Apple.Native
{

View File

@@ -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
{

View File

@@ -741,6 +741,7 @@ namespace osu.Framework.Platform
CacheStorage = GetDefaultGameStorage().GetStorageForDirectory("cache");
SetupForRun();
game.SetupLogging(Storage, CacheStorage);
populateInputHandlers();

View File

@@ -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();

View File

@@ -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)

View File

@@ -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<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore)
=> new MacOSTextureLoaderStore(underlyingStore);
protected override void Swap()
{
base.Swap();

View File

@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<byte[]> store)
: base(store)
{
}
protected override unsafe Image<TPixel> ImageFromStream<TPixel>(Stream stream)
{
using (NSAutoreleasePool.Init())
{
int length = (int)(stream.Length - stream.Position);
var nativeData = NSMutableData.FromLength(length);
var bytesSpan = new Span<byte>(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<TPixel>(cgImage);
}
}
}
}

View File

@@ -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));

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(() =>

View File

@@ -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));

View File

@@ -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));

View File

@@ -8,8 +8,8 @@ namespace osu.Framework.Testing.Input
{
public class ManualTextInputSource : TextInputSource
{
public readonly Queue<bool> ActivationQueue = new Queue<bool>();
public readonly Queue<bool> EnsureActivatedQueue = new Queue<bool>();
public readonly Queue<TextInputProperties> ActivationQueue = new Queue<TextInputProperties>();
public readonly Queue<TextInputProperties> EnsureActivatedQueue = new Queue<TextInputProperties>();
public readonly Queue<bool> DeactivationQueue = new Queue<bool>();
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()

View File

@@ -38,7 +38,7 @@
<PackageReference Include="ppy.osuTK.NS20" Version="1.0.211" />
<PackageReference Include="StbiSharp" Version="1.1.0" />
<PackageReference Include="ppy.SDL2-CS" Version="1.0.741-alpha" />
<PackageReference Include="ppy.SDL3-CS" Version="2024.1128.0" />
<PackageReference Include="ppy.SDL3-CS" Version="2025.104.0" />
<PackageReference Include="ppy.osu.Framework.SourceGeneration" Version="2024.1128.0" />
<!-- DO NOT use ProjectReference for native packaging project.