TextEdit control & a bunch of other stuff (#3436)

This commit is contained in:
Pieter-Jan Briers
2022-11-12 03:12:49 +01:00
committed by GitHub
parent db95c6284b
commit e34935c9e2
56 changed files with 4244 additions and 532 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Robust.Shared.Map;
namespace Robust.Shared.Input
@@ -7,6 +8,7 @@ namespace Robust.Shared.Input
/// Event data values for a bound key state change.
/// </summary>
[Virtual]
[DebuggerDisplay("{Function}, {State}, CF: {CanFocus}, H: {Handled}")]
public class BoundKeyEventArgs : EventArgs
{
/// <summary>

View File

@@ -70,6 +70,8 @@ namespace Robust.Shared.Input
this.reflectionManager = reflectionManager;
}
internal IEnumerable<BoundKeyFunction> AllKeyFunctions => KeyFunctionsList;
public void PopulateKeyFunctionsMap()
{
if (KeyFunctionsMap.Count != 0)

View File

@@ -46,6 +46,8 @@ namespace Robust.Shared.Input
// Cursor keys in LineEdit and such.
public static readonly BoundKeyFunction TextCursorLeft = "TextCursorLeft";
public static readonly BoundKeyFunction TextCursorRight = "TextCursorRight";
public static readonly BoundKeyFunction TextCursorUp = "TextCursorUp";
public static readonly BoundKeyFunction TextCursorDown = "TextCursorDown";
public static readonly BoundKeyFunction TextCursorWordLeft = "TextCursorWordLeft";
public static readonly BoundKeyFunction TextCursorWordRight = "TextCursorWordRight";
public static readonly BoundKeyFunction TextCursorBegin = "TextCursorBegin";
@@ -55,12 +57,15 @@ namespace Robust.Shared.Input
public static readonly BoundKeyFunction TextCursorSelect = "TextCursorSelect";
public static readonly BoundKeyFunction TextCursorSelectLeft = "TextCursorSelectLeft";
public static readonly BoundKeyFunction TextCursorSelectRight = "TextCursorSelectRight";
public static readonly BoundKeyFunction TextCursorSelectUp = "TextCursorSelectUp";
public static readonly BoundKeyFunction TextCursorSelectDown = "TextCursorSelectDown";
public static readonly BoundKeyFunction TextCursorSelectWordLeft = "TextCursorSelectWordLeft";
public static readonly BoundKeyFunction TextCursorSelectWordRight = "TextCursorSelectWordRight";
public static readonly BoundKeyFunction TextCursorSelectBegin = "TextCursorSelectBegin";
public static readonly BoundKeyFunction TextCursorSelectEnd = "TextCursorSelectEnd";
public static readonly BoundKeyFunction TextBackspace = "TextBackspace";
public static readonly BoundKeyFunction TextNewline = "TextNewline";
public static readonly BoundKeyFunction TextSubmit = "TextSubmit";
public static readonly BoundKeyFunction TextSelectAll = "TextSelectAll";
public static readonly BoundKeyFunction TextCopy = "TextCopy";

View File

@@ -100,6 +100,11 @@ namespace Robust.Shared.Utility
_tags.Clear();
}
public FormattedMessageRuneEnumerator EnumerateRunes()
{
return new FormattedMessageRuneEnumerator(this);
}
/// <returns>The string without markup tags.</returns>
public override string ToString()
{
@@ -192,4 +197,60 @@ namespace Robust.Shared.Utility
public Tag this[int index] => _tags[index];
}
}
public struct FormattedMessageRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
{
private readonly FormattedMessage _msg;
private List<FormattedMessage.Tag>.Enumerator _tagEnumerator;
private StringRuneEnumerator _runeEnumerator;
internal FormattedMessageRuneEnumerator(FormattedMessage msg)
{
_msg = msg;
_tagEnumerator = msg.Tags.GetEnumerator();
// Rune enumerator will immediately give false on first iteration so I dont' need to special case anything.
_runeEnumerator = "".EnumerateRunes();
}
public IEnumerator<Rune> GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool MoveNext()
{
while (!_runeEnumerator.MoveNext())
{
FormattedMessage.TagText text;
while (true)
{
var result = _tagEnumerator.MoveNext();
if (!result)
return false;
if (_tagEnumerator.Current is not FormattedMessage.TagText { Text.Length: > 0 } nextText)
continue;
text = nextText;
break;
}
_runeEnumerator = text.Text.EnumerateRunes();
}
return true;
}
public void Reset()
{
_tagEnumerator = _msg.Tags.GetEnumerator();
_runeEnumerator = "".EnumerateRunes();
}
public Rune Current => _runeEnumerator.Current;
object IEnumerator.Current => Current;
void IDisposable.Dispose()
{
}
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Robust.Shared.Utility;
internal static class StringEnumerateHelpers
{
internal struct SubstringRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
{
private readonly string _source;
private int _nextChar;
private Rune _current;
public SubstringRuneEnumerator(string source, int firstChar)
{
_source = source;
_nextChar = firstChar;
_current = default;
}
public bool MoveNext()
{
if (_nextChar >= _source.Length)
return false;
if (!Rune.TryGetRuneAt(_source, _nextChar, out _current))
_current = Rune.ReplacementChar;
_nextChar += _current.Utf16SequenceLength;
return true;
}
public void Reset()
{
throw new NotSupportedException();
}
public readonly Rune Current => _current;
object IEnumerator.Current => Current;
public void Dispose()
{
// Nada.
}
public SubstringRuneEnumerator GetEnumerator() => this;
IEnumerator<Rune> IEnumerable<Rune>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
internal struct SubstringReverseRuneEnumerator : IEnumerator<Rune>, IEnumerable<Rune>
{
private string _source;
// Contains the next char to return.
// If the next char is actually a (valid) surrogate pair, this is INSIDE the pair,
// and MoveNext() has to skip more.
private int _nextChar;
private Rune _current;
public SubstringReverseRuneEnumerator(string source, int startChar)
{
_source = source;
_nextChar = startChar - 1;
_current = default;
}
public bool MoveNext()
{
if (_nextChar < 0)
return false;
var chr = _source[_nextChar];
if (!char.IsSurrogate(chr))
{
_current = new Rune(chr);
}
else if (char.IsLowSurrogate(chr) && _nextChar >= 1)
{
var prevChr = _source[_nextChar - 1];
if (char.IsHighSurrogate(prevChr))
_current = new Rune(prevChr, chr);
else
_current = Rune.ReplacementChar;
}
else
{
_current = Rune.ReplacementChar;
}
_nextChar -= _current.Utf16SequenceLength;
return true;
}
public void Reset()
{
throw new NotSupportedException();
}
public Rune Current => _current;
object IEnumerator.Current => Current;
public void Dispose()
{
// Nada.
}
public SubstringReverseRuneEnumerator GetEnumerator() => this;
IEnumerator<Rune> IEnumerable<Rune>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -0,0 +1,561 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
namespace Robust.Shared.Utility;
/// <summary>
/// A binary tree data structure for efficient storage of large mutable text.
/// </summary>
/// <remarks>
/// <para>
/// <see href="https://en.wikipedia.org/wiki/Rope_(data_structure)">Read the Wikipedia article, nerd</see>
/// Also read the original paper, it's useful too.
/// </para>
/// <para>
/// Like strings, ropes are immutable and all "mutating" operations return new copies.
/// </para>
/// <para>
/// All indexing functions use <see langword="long"/> indices.
/// While individual rope leaves cannot be larger than an <see langword="int"/>, ropes with many leaves may exceed that.
/// </para>
/// </remarks>
public static class Rope
{
internal static readonly int[] FibonacciSequence =
{
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,
17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309,
3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141,
267914296, 433494437, 701408733, 1134903170, 1836311903
};
/// <summary>
/// Calculate the total text length of the rope given.
/// </summary>
/// <remarks>
/// For a balanced tree, this is O(log n).
/// </remarks>
[Pure]
public static long CalcTotalLength(Node? node)
{
return node switch
{
Branch branch => branch.Weight + CalcTotalLength(branch.Right),
Leaf leaf => leaf.Weight,
_ => 0
};
}
// TODO: Move to struct enumerator with managed stack memory.
/// <summary>
/// Enumerate all leaves in the rope from left to right.
/// </summary>
public static IEnumerable<Leaf> CollectLeaves(Node node)
{
var stack = new Stack<Branch>();
var leaf = RunTillLeaf(stack, node);
yield return leaf;
while (stack.TryPop(out var branch))
{
if (branch.Right == null)
continue;
leaf = RunTillLeaf(stack, branch.Right);
yield return leaf;
}
static Leaf RunTillLeaf(Stack<Branch> stack, Node node)
{
while (node is Branch branch)
{
stack.Push(branch);
node = branch.Left;
}
return (Leaf)node;
}
}
/// <summary>
/// Enumerate all leaves in the rope from right to left.
/// </summary>
public static IEnumerable<Leaf> CollectLeavesReverse(Node node)
{
var stack = new Stack<Branch>();
var leaf = RunTillLeaf(stack, node);
if (leaf != null)
yield return leaf;
while (stack.TryPop(out var branch))
{
leaf = RunTillLeaf(stack, branch.Left);
if (leaf != null)
yield return leaf;
}
static Leaf? RunTillLeaf(Stack<Branch> stack, Node? node)
{
while (node is Branch branch)
{
stack.Push(branch);
node = branch.Right;
}
return (Leaf?)node;
}
}
// TODO: Move to struct enumerator with managed stack memory.
/// <summary>
/// Enumerate all text runes in the rope from left to right.
/// </summary>
public static IEnumerable<Rune> EnumerateRunes(Node node)
{
foreach (var leaf in CollectLeaves(node))
{
foreach (var rune in leaf.Text.EnumerateRunes())
{
yield return rune;
}
}
}
/// <summary>
/// Enumerate all text runes in the rope from left to right, starting at the specified position.
/// </summary>
public static IEnumerable<Rune> EnumerateRunes(Node node, long startPos)
{
var pos = 0L;
// Phase 1: skip over whole leaves that are before the start position.
// TODO: Ideally we would navigate the binary tree properly instead of starting from the far left.
// ReSharper disable once GenericEnumeratorNotDisposed
var leaves = CollectLeaves(node).GetEnumerator();
while (leaves.MoveNext())
{
var leaf = leaves.Current;
if (pos + leaf.Weight >= startPos)
{
goto startLeafFound;
}
pos += leaf.Weight;
}
// Didn't find a starting leaf, must mean that startPos >= text length. Oh well?
yield break;
startLeafFound:
// Phase 2: start halfway through the current leaf.
{
foreach (var rune in leaves.Current.Text.EnumerateRunes())
{
if (pos >= startPos)
{
yield return rune;
}
pos += rune.Utf16SequenceLength;
}
}
// Phase 3: just return everything from here on out.
while (leaves.MoveNext())
{
var leaf = leaves.Current;
foreach (var rune in leaf.Text.EnumerateRunes())
{
yield return rune;
}
}
}
/// <summary>
/// Enumerate all the runes in the rope, from right to left.
/// </summary>
public static IEnumerable<Rune> EnumerateRunesReverse(Node node)
{
foreach (var leaf in CollectLeavesReverse(node))
{
var enumerator = new StringEnumerateHelpers.SubstringReverseRuneEnumerator(leaf.Text, leaf.Text.Length);
while (enumerator.MoveNext())
{
yield return enumerator.Current;
}
}
}
/// <summary>
/// Enumerate all text runes in the rope from right to left, starting at the specified position.
/// </summary>
public static IEnumerable<Rune> EnumerateRunesReverse(Node node, long endPos)
{
var pos = CalcTotalLength(node);
// TODO: Actually start at the position instead of skipping like a worse linked list thanks.
foreach (var rune in EnumerateRunesReverse(node))
{
if (pos <= endPos)
{
yield return rune;
}
pos -= rune.Utf16SequenceLength;
}
}
/// <summary>
/// Check whether the given rope is sufficiently balanced to avoid bad performance.
/// </summary>
[Pure]
public static bool IsBalanced(Node node)
{
var depth = node.Depth;
if (depth > FibonacciSequence.Length - 2)
return false;
return FibonacciSequence[depth + 2] <= node.Weight;
}
/// <summary>
/// Ensure the rope is balanced to ensure decent performance on various operations.
/// </summary>
/// <remarks>
/// If the rope is already balanced, this method does nothing.
/// </remarks>
[Pure]
public static Node Rebalance(Node node)
{
if (IsBalanced(node))
return node;
var leaves = CollectLeaves(node).ToArray();
return Merge(leaves);
static Node Merge(ReadOnlySpan<Leaf> leaves)
{
if (leaves.Length == 1)
return leaves[0];
if (leaves.Length == 2)
return new Branch(leaves[0], leaves[1]);
var mid = leaves.Length / 2;
return new Branch(Merge(leaves[..mid]), Merge(leaves[mid..]));
}
}
/// <summary>
/// Get a <see langword="char" /> at the specified index in the rope.
/// </summary>
/// <remarks>
/// For a balanced tree, this is O(log n).
/// </remarks>
[Pure]
public static char Index(Node rope, long index)
{
switch (rope)
{
case Branch branch:
if (branch.Weight > index)
return Index(branch.Left, index);
if (branch.Right == null)
throw new IndexOutOfRangeException();
return Index(branch.Right, index - branch.Weight);
case Leaf leaf:
return leaf.Text[(int)index];
default:
throw new ArgumentOutOfRangeException(nameof(rope));
}
}
/// <summary>
/// Create a new rope with text spliced in at an index.
/// </summary>
/// <param name="rope">The rope to splice text into.</param>
/// <param name="index">The position the inserted text should start at.</param>
/// <param name="value">The text to insert.</param>
/// <returns>The new rope containing the spliced data.</returns>
[Pure]
public static Node Insert(Node rope, long index, string value)
{
var (left, right) = Split(rope, index);
return Concat(left, Concat(new Leaf(value), right));
}
/// <summary>
/// Create a new rope concatenating two given ropes.
/// </summary>
[Pure]
public static Node Concat(Node left, Node right)
{
return new Branch(left, right);
}
/// <summary>
/// Create a new rope concatenating a rope and a string.
/// </summary>
[Pure]
public static Node Concat(Node left, string right)
{
return Concat(left, new Leaf(right));
}
/// <summary>
/// Create a new rope concatenating a string with a rope.
/// </summary>
[Pure]
public static Node Concat(string left, Node right)
{
return Concat(new Leaf(left), right);
}
/// <summary>
/// Return two new ropes split from the given rope at a specified index.
/// </summary>
[Pure]
public static (Node left, Node right) Split(Node rope, long index)
{
switch (rope)
{
case Branch branch:
{
if (branch.Weight > index)
{
var (left, right) = Split(branch.Left, index);
return (
Rebalance(left),
Rebalance(new Branch(right, branch.Right))
);
}
if (branch.Weight < index)
{
var (left, right) = Split(branch.Right ?? Leaf.Empty, index - branch.Weight);
return (
Rebalance(new Branch(branch.Left, left)),
Rebalance(right)
);
}
return (branch.Left, branch.Right ?? Leaf.Empty);
}
case Leaf leaf:
{
var left = new Leaf(leaf.Text[..(int)index]);
var right = new Leaf(leaf.Text[(int)index..]);
return (left, right);
}
default:
throw new ArgumentOutOfRangeException(nameof(rope));
}
}
/// <summary>
/// Create a new rope with a slice of text removed.
/// </summary>
/// <param name="rope">The rope to copy.</param>
/// <param name="start">The position to start removing chars at.</param>
/// <param name="length">How many chars to remove.</param>
[Pure]
public static Node Delete(Node rope, long start, long length)
{
var (left, _) = Split(rope, start);
var (_, right) = Split(rope, start + length);
return Concat(left, right);
}
/// <summary>
/// Create a new rope with a given slice of text replaced with a new string.
/// </summary>
/// <param name="rope">The rope to copy.</param>
/// <param name="start">The position to start removing characters at, and insert the new text at.</param>
/// <param name="length">How many characters from the original rope to remove.</param>
/// <param name="text">The new text to insert at the start position.</param>
[Pure]
public static Node ReplaceSubstring(Node rope, long start, long length, string text)
{
var (left, mid) = Split(rope, start);
var (_, right) = Split(mid, length);
return Concat(left, Concat(text, right));
}
/// <summary>
/// Try to fetch a <see cref="Rune"/> at a certain position in the rune.
/// Fails if the given position is inside a surrogate pair.
/// </summary>
[Pure]
public static bool TryGetRuneAt(Node rope, long index, out Rune value)
{
var chr = Index(rope, index);
if (!char.IsSurrogate(chr))
{
value = new Rune(chr);
return true;
}
if (char.IsLowSurrogate(chr))
{
value = default;
return false;
}
// TODO: throws if a high surrogate is at the very end of the rope.
var lowChr = Index(rope, index + 1);
if (!char.IsLowSurrogate(lowChr))
{
value = default;
return false;
}
value = new Rune(chr, lowChr);
return true;
}
/// <summary>
/// Collapse the rope into a single string instance.
/// </summary>
/// <exception cref="OverflowException">The given rope is too large to fit in a single string.</exception>
[Pure]
public static string Collapse(Node rope)
{
var length = CalcTotalLength(rope);
return string.Create(checked((int)length), rope, static (span, node) =>
{
foreach (var leaf in CollectLeaves(node))
{
var text = leaf.Text;
text.CopyTo(span);
span = span[text.Length..];
}
});
}
/// <summary>
/// Collapse a substring of a rope into a single string instance.
/// </summary>
/// <param name="rope">The rope to collapse part of.</param>
/// <param name="range">The range of the substring to collapse.</param>
/// <exception cref="OverflowException">The given rope is too large to fit in a single string.</exception>
[Pure]
public static string CollapseSubstring(Node rope, Range range)
{
// TODO: Optimize
return Collapse(rope)[range];
}
/// <summary>
/// Offset a cursor position in a rope to the left, skipping over the middle of surrogate pairs.
/// </summary>
[Pure]
public static long RuneShiftLeft(long index, Node rope)
{
index -= 1;
if (char.IsLowSurrogate(Index(rope, index)))
index -= 1;
return index;
}
/// <summary>
/// Offset a cursor position in a rope to the right, skipping over the middle of surrogate pairs.
/// </summary>
[Pure]
public static long RuneShiftRight(long index, Node rope)
{
index += 1;
// Before you confuse yourself on "shouldn't this be high surrogate since shifting left checks low"
// (Because yes, I did myself too a week after writing it)
// char.IsLowSurrogate(_text[_cursorPosition]) means "is the cursor between a surrogate pair"
// because we ALREADY moved.
if (char.IsLowSurrogate(Index(rope, index)))
index += 1;
return index;
}
/// <summary>
/// Returns true if the given rope is either null or empty (length 0).
/// </summary>
[Pure]
public static bool IsNullOrEmpty([NotNullWhen(false)] Node? rope)
{
if (rope == null)
return true;
return CalcTotalLength(rope) == 0;
}
/// <summary>
/// A nope in a rope. This is either a <see cref="Leaf"/> or a <see cref="Branch"/>.
/// </summary>
public abstract class Node
{
public abstract long Weight { get; }
/// <summary>
/// The depth of the deepest leaf in this node tree. A leaf has depth 0, and a branch one above 1, etc...
/// </summary>
public abstract short Depth { get; }
}
/// <summary>
/// A leaf contains a string of text.
/// </summary>
[DebuggerDisplay("W: {Weight}, Text: {Text}")]
public sealed class Leaf : Node
{
public static readonly Leaf Empty = new("");
public string Text { get; }
public Leaf(string text)
{
Text = text;
}
public override long Weight => Text.Length;
public override short Depth => 0;
}
/// <summary>
/// A branch contains other nodes to the left and right.
/// </summary>
[DebuggerDisplay("W: {Weight}")]
public sealed class Branch : Node
{
public Node Left { get; }
public Node? Right { get; }
public override long Weight { get; }
public override short Depth { get; }
public Branch(Node left, Node? right)
{
Left = left;
Right = right;
Weight = CalcTotalLength(left);
Depth = checked((short)(Math.Max(left.Depth, right?.Depth ?? 0) + 1));
}
}
}