添加对 Core 的直接引用

This commit is contained in:
OLDREDSTONE
2025-03-25 15:49:57 +08:00
parent 24907abedb
commit 06c19bf10a
296 changed files with 24961 additions and 2 deletions
@@ -0,0 +1,94 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Defines the interface for rich string styles.
/// </summary>
/// <typeparam name="TSelf">The type that implements this interface.</typeparam>
public interface IRDRichStringStyle<TSelf> : IEqualityOperators<TSelf, TSelf, bool>, IEquatable<TSelf>
where TSelf : IRDRichStringStyle<TSelf>
{
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns>True if the specified object is equal to the current object; otherwise, false.</returns>
bool Equals([NotNullWhen(true)] object? obj) => obj is TSelf e && Equals(e);
/// <summary>
/// Gets the closing tag for the specified name.
/// </summary>
/// <param name="name">The name of the tag.</param>
/// <returns>The closing tag for the specified name.</returns>
static string GetCloseTag(string name) => $"</{name}>";
/// <summary>
/// Gets the opening tag for the specified name and optional argument.
/// </summary>
/// <param name="name">The name of the tag.</param>
/// <param name="arg">The optional argument for the tag.</param>
/// <returns>The opening tag for the specified name and optional argument.</returns>
static string GetOpenTag(string name, string? arg = null)=> arg is null ? $"<{name}>" : $"<{name}={arg}>";
/// <summary>
/// Generates an XML tag representing the differences between two <see cref="RDDialoguePhraseStyle"/> instances.
/// </summary>
/// <param name="before">The initial <see cref="RDDialoguePhraseStyle"/> instance.</param>
/// <param name="after">The modified <see cref="RDDialoguePhraseStyle"/> instance.</param>
/// <returns>A string containing the XML tag that represents the differences between the two instances.</returns>
static abstract string GetXmlTag(TSelf before, TSelf after);
/// <summary>
/// Resets the property of the rich string style based on the provided name.
/// </summary>
/// <param name="name">The name of the property to reset.</param>
/// <returns>True if the property was successfully reset; otherwise, false.</returns>
bool ResetProperty(string name);
/// <summary>
/// Sets the property of the rich string style based on the provided name and value.
/// </summary>
/// <param name="name">The name of the property to set.</param>
/// <param name="value">The value to set for the property.</param>
/// <returns>True if the property was successfully set; otherwise, false.</returns>
bool SetProperty(string name, string value);
/// <summary>
/// Tries to add a tag to the specified string based on the provided name and boolean values.
/// </summary>
/// <param name="tag">The string to which the tag will be added.</param>
/// <param name="name">The name of the tag.</param>
/// <param name="before">A boolean value indicating whether the tag is before.</param>
/// <param name="after">A boolean value indicating whether the tag is after.</param>
static void TryAddTag(ref string tag, string name, bool before, bool after)
{
if (before != after)
tag += after
? GetOpenTag(name)
: GetCloseTag(name);
}
/// <summary>
/// Tries to add a tag to the specified string based on the provided name and optional string values.
/// </summary>
/// <param name="tag">The string to which the tag will be added.</param>
/// <param name="name">The name of the tag.</param>
/// <param name="before">An optional string value indicating the tag before.</param>
/// <param name="after">An optional string value indicating the tag after.</param>
static void TryAddTag(ref string tag, string name, string? before, string? after)
{
if (before != after)
tag += after is null
? GetCloseTag(name)
: before is null
? GetOpenTag(name, after)
: GetCloseTag(name) + GetOpenTag(name, after);
}
/// <summary>
/// Gets a value indicating whether the style has a phrase.
/// </summary>
static abstract bool HasPhrase { get; }
}
}
@@ -0,0 +1,43 @@
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a line of rich text with a specific style.
/// </summary>
/// <typeparam name="TStyle">The type of the style applied to the rich text.</typeparam>
public interface IRDRichTextLine<TStyle> where TStyle : IRDRichStringStyle<TStyle>, new()
{
/// <summary>
/// Gets or sets the <see cref="RDLine{TStyle}"/> at the specified index.
/// </summary>
/// <param name="index">The index of the rich text line.</param>
/// <returns>The rich text line at the specified index.</returns>
RDLine<TStyle> this[Index index] { get; set; }
/// <summary>
/// Gets or sets the <see cref="RDLine{TStyle}"/> within the specified range.
/// </summary>
/// <param name="range">The range of the rich text lines.</param>
/// <returns>The rich text lines within the specified range.</returns>
RDLine<TStyle> this[Range range] { get; set; }
/// <summary>
/// Gets the length of the rich text line.
/// </summary>
int Length { get; }
/// <summary>
/// Concatenates multiple <see cref="RDLine{TStyle}"/> instances into a single instance.
/// </summary>
/// <param name="lines">The rich text lines to concatenate.</param>
/// <returns>A new <see cref="RDLine{TStyle}"/> containing the concatenated content.</returns>
static abstract RDLine<TStyle> Concat(params RDLine<TStyle>[] lines);
/// <summary>
/// Deserializes a string into an <see cref="RDLine{TStyle}"/>.
/// </summary>
/// <param name="text">The string to deserialize.</param>
/// <returns>A new <see cref="RDLine{TStyle}"/> containing the deserialized content.</returns>
static abstract RDLine<TStyle> Deserialize(string text);
/// <summary>
/// Serializes the current <see cref="RDLine{TStyle}"/> instance to a string.
/// </summary>
/// <returns>A string representation of the current <see cref="RDLine{TStyle}"/> instance.</returns>
string Serialize();
}
}
@@ -0,0 +1,110 @@
using System.Text;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a line of dialogue, which consists of multiple dialogue components.
/// </summary>
public class RDDialogueBlock
{
/// <summary>
/// Gets or sets the character speaking the dialogue line.
/// </summary>
public string? Character { get; set; }
/// <summary>
/// Gets or sets the expression of the character.
/// </summary>
public string? Expression { get; set; }
/// <summary>
/// Gets or sets the content of the dialogue line.
/// </summary>
/// <value>
/// The content of the dialogue line, represented as an <see cref="RDLine{TStyle}"/>.
/// </value>
public RDLine<RDDialoguePhraseStyle> Content { get; set; } = "";
/// <summary>
/// Serializes the dialogue line to a string.
/// </summary>
/// <returns>A string representation of the dialogue line.</returns>
public string Serialize()
{
var sb = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Character))
{
sb.Append(Character);
if (!string.IsNullOrWhiteSpace(Expression))
{
sb.Append('_').Append(Expression);
}
sb.Append(':');
}
sb.Append(Content.Serialize());
return sb.ToString();
}
/// <summary>
/// Deserializes a string into a <see cref="RDDialogueBlock"/> instance.
/// </summary>
/// <param name="str">The string to deserialize.</param>
/// <returns>A new <see cref="RDDialogueBlock"/> containing the deserialized content.</returns>
/// <exception cref="ArgumentNullException">Thrown when the input string is null.</exception>
/// <exception cref="FormatException">Thrown when the input string has an invalid format.</exception>
public static RDDialogueBlock Deserialize(string str)
{
str = str.Trim();
RDDialogueBlock line = new();
int mi = str.IndexOf(':');
if (mi != -1)
{
string character = str[..mi];
if (character.Contains('_'))
{
string[] parts = character.Split('_', 2);
character = parts[0];
line.Expression = parts[1];
}
line.Character = character;
}
line.Content = RDLine<RDDialoguePhraseStyle>.Deserialize(str[(mi + 1)..]);
return line;
}
/// <summary>
/// Deserializes a string into a <see cref="RDDialogueBlock"/> instance, using a lookup of valid expressions.
/// </summary>
/// <param name="str">The string to deserialize.</param>
/// <param name="expressions">A lookup of valid expressions for each character.</param>
/// <returns>A new <see cref="RDDialogueBlock"/> containing the deserialized content.</returns>
/// <exception cref="ArgumentNullException">Thrown when the input string is null.</exception>
/// <exception cref="FormatException">Thrown when the input string has an invalid format.</exception>
public static RDDialogueBlock Deserialize(string str, ILookup<string, string> expressions)
{
str = str.Trim();
RDDialogueBlock line = new();
int mi = str.IndexOf(':');
if (mi != -1)
{
string character = str[..mi];
string expression = "";
if (character.Contains('_'))
{
string[] parts = character.Split('_', 2);
character = parts[0];
expression = parts[1];
}
else
character = str;
if (!expressions.Contains(character))
{
line.Content = RDLine<RDDialoguePhraseStyle>.Deserialize(str);
return line;
}
line.Character = character;
if (expressions[character].Contains(expression))
line.Expression = expression;
}
line.Content = RDLine<RDDialoguePhraseStyle>.Deserialize(str[(mi + 1)..]);
return line;
}
/// <inheritdoc/>
public override string ToString() => $"{Character}({Expression}):{Content}";
}
}
@@ -0,0 +1,31 @@
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a list of dialogue lines.
/// </summary>
public class RDDialogueExchange : List<RDDialogueBlock>
{
/// <summary>
/// Serializes the dialogue list to a string.
/// </summary>
/// <returns>A string representation of the dialogue list.</returns>
public string Serialize() => string.Join('\n', this.Select(i => i.Serialize()));
/// <summary>
/// Deserializes a string into a <see cref="RDDialogueExchange"/> instance.
/// </summary>
/// <param name="text">The string to deserialize.</param>
/// <returns>A new <see cref="RDDialogueExchange"/> containing the deserialized dialogue lines.</returns>
public static RDDialogueExchange Deserialize(string text) => [.. text.Split("\r\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(RDDialogueBlock.Deserialize)];
/// <summary>
/// Deserializes a string into a <see cref="RDDialogueExchange"/> instance, using a lookup of valid expressions.
/// </summary>
/// <param name="text">The string to deserialize.</param>
/// <param name="expressions">A lookup of valid expressions for each character.</param>
/// <returns>A new <see cref="RDDialogueExchange"/> containing the deserialized dialogue lines.</returns>
/// <exception cref="ArgumentNullException">Thrown when the input string is null.</exception>
/// <exception cref="FormatException">Thrown when the input string has an invalid format.</exception>
public static RDDialogueExchange Deserialize(string text, ILookup<string, string> expressions) => [.. text.Split("\r\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(i => RDDialogueBlock.Deserialize(i, expressions))];
///<inheritdoc/>
public override string ToString() => string.Join('\n', this.Select(i => i.ToString()));
}
}
@@ -0,0 +1,276 @@
using System.Diagnostics.CodeAnalysis;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents the style of a rich string.
/// </summary>
public struct RDDialoguePhraseStyle : IRDRichStringStyle<RDDialoguePhraseStyle>
{
/// <summary>
/// Gets or sets the color of the text.
/// </summary>
public RDColor? Color { get; set; }
/// <summary>
/// Gets or sets the speed of the text animation.
/// </summary>
public float? Speed { get; set; }
/// <summary>
/// Gets or sets the volume of the text.
/// </summary>
public float? Volume { get; set; }
/// <summary>
/// Gets or sets the pitch of the text.
/// </summary>
public float? Pitch { get; set; }
/// <summary>
/// Gets or sets the pitch range of the text.
/// </summary>
public float? PitchRange { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should shake.
/// </summary>
public bool Shake { get; set; }
/// <summary>
/// Gets or sets the radius of the shake effect.
/// </summary>
public float? ShakeRadius { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should have a wave effect.
/// </summary>
public bool Wave { get; set; }
/// <summary>
/// Gets or sets the height of the wave effect.
/// </summary>
public float? WaveHeight { get; set; }
/// <summary>
/// Gets or sets the speed of the wave effect.
/// </summary>
public float? WaveSpeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should have a swirl effect.
/// </summary>
public bool Swirl { get; set; }
/// <summary>
/// Gets or sets the radius of the swirl effect.
/// </summary>
public float? SwirlRadius { get; set; }
/// <summary>
/// Gets or sets the speed of the swirl effect.
/// </summary>
public float? SwirlSpeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should be sticky.
/// </summary>
public bool Sticky { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should be loud.
/// </summary>
public bool Loud { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should be bold.
/// </summary>
public bool Bold { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the text should be whispered.
/// </summary>
public bool Whisper { get; set; }
/// <inheritdoc/>
public static bool HasPhrase => true;
/// <summary>
/// Sets the property of the rich string style based on the provided name and value.
/// </summary>
/// <param name="name">The name of the property to set.</param>
/// <param name="value">The value to set for the property.</param>
/// <returns>True if the property was successfully set; otherwise, false.</returns>
public bool SetProperty(string name, string value)
{
switch (name)
{
case "color":
if (RDColor.TryFromName(value, out RDColor color))
Color = color;
else if (RDColor.TryFromRgba(value, out color))
Color = color;
else
return false;
break;
case "speed":
Speed = float.Parse(value);
break;
case "volume":
Volume = float.Parse(value);
break;
case "pitch":
Pitch = float.Parse(value);
break;
case "pitchRange":
PitchRange = float.Parse(value);
break;
case "shake":
Shake = bool.Parse(value);
break;
case "shakeRadius":
ShakeRadius = float.Parse(value);
break;
case "wave":
Wave = bool.Parse(value);
break;
case "waveHeight":
WaveHeight = float.Parse(value);
break;
case "waveSpeed":
WaveSpeed = float.Parse(value);
break;
case "swirl":
Swirl = bool.Parse(value);
break;
case "swirlRadius":
SwirlRadius = float.Parse(value);
break;
case "swirlSpeed":
SwirlSpeed = float.Parse(value);
break;
case "sticky":
Sticky = bool.Parse(value);
break;
case "loud":
Loud = bool.Parse(value);
break;
case "bold":
Bold = bool.Parse(value);
break;
case "whisper":
Whisper = bool.Parse(value);
break;
default:
return false;
}
return true;
}
/// <summary>
/// Removes the property of the rich string style based on the provided name.
/// </summary>
/// <param name="name">The name of the property to remove.</param>
/// <returns>True if the property was successfully removed; otherwise, false.</returns>
public bool ResetProperty(string name)
{
switch (name)
{
case "color":
Color = null;
break;
case "speed":
Speed = null;
break;
case "volume":
Volume = null;
break;
case "pitch":
Pitch = null;
break;
case "pitchRange":
PitchRange = null;
break;
case "shake":
Shake = false;
break;
case "shakeRadius":
ShakeRadius = null;
break;
case "wave":
Wave = false;
break;
case "waveHeight":
WaveHeight = null;
break;
case "waveSpeed":
WaveSpeed = null;
break;
case "swirl":
Swirl = false;
break;
case "swirlRadius":
SwirlRadius = null;
break;
case "swirlSpeed":
SwirlSpeed = null;
break;
case "sticky":
Sticky = false;
break;
case "loud":
Loud = false;
break;
case "bold":
Bold = false;
break;
case "whisper":
Whisper = false;
break;
default:
return false;
}
return true;
}
/// <inheritdoc/>
public static string GetXmlTag(RDDialoguePhraseStyle before, RDDialoguePhraseStyle after)
{
string tag = "";
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "color",
before.Color?.TryGetName(out string[] namesbefore) == true
? namesbefore[0].ToLower()
: before.Color?.ToString(before.Color?.A == 255 ? "#RRGGBB" : "#RRGGBBAA"),
after.Color?.TryGetName(out string[] namesafter) == true
? namesafter[0].ToLower()
: after.Color?.ToString(after.Color?.A == 255 ? "#RRGGBB" : "#RRGGBBAA"));
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "speed", before.Speed?.ToString(), after.Speed?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "volume", before.Volume?.ToString(), after.Volume?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "pitch", before.Pitch?.ToString(), after.Pitch?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "pitchRange", before.PitchRange?.ToString(), after.PitchRange?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "shake", before.Shake, after.Shake);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "shakeRadius", before.ShakeRadius?.ToString(), after.ShakeRadius?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "wave", before.Wave, after.Wave);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "waveHeight", before.WaveHeight?.ToString(), after.WaveHeight?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "waveSpeed", before.WaveSpeed?.ToString(), after.WaveSpeed?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "swirl", before.Swirl, after.Swirl);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "swirlRadius", before.SwirlRadius?.ToString(), after.SwirlRadius?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "swirlSpeed", before.SwirlSpeed?.ToString(), after.SwirlSpeed?.ToString());
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "sticky", before.Sticky, after.Sticky);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "loud", before.Loud, after.Loud);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "bold", before.Bold, after.Bold);
IRDRichStringStyle<RDDialoguePhraseStyle>.TryAddTag(ref tag, "whisper", before.Whisper, after.Whisper);
return tag;
}
/// <inheritdoc/>
public static bool operator ==(RDDialoguePhraseStyle left, RDDialoguePhraseStyle right) =>
left.Color == right.Color
&& left.Speed == right.Speed
&& left.Volume == right.Volume
&& left.Pitch == right.Pitch
&& left.PitchRange == right.PitchRange
&& left.Shake == right.Shake
&& left.ShakeRadius == right.ShakeRadius
&& left.Wave == right.Wave
&& left.WaveHeight == right.WaveHeight
&& left.WaveSpeed == right.WaveSpeed
&& left.Swirl == right.Swirl
&& left.SwirlRadius == right.SwirlRadius
&& left.SwirlSpeed == right.SwirlSpeed
&& left.Sticky == right.Sticky
&& left.Loud == right.Loud
&& left.Bold == right.Bold
&& left.Whisper == right.Whisper;
/// <inheritdoc/>
public static bool operator !=(RDDialoguePhraseStyle left, RDDialoguePhraseStyle right) => !(left == right);
/// <inheritdoc/>
public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is RDDialoguePhraseStyle e && base.Equals(e);
/// <inheritdoc/>
public readonly bool Equals(RDDialoguePhraseStyle other) => this == other;
/// <inheritdoc/>
public readonly override int GetHashCode()
{
return base.GetHashCode();
}
}
}
@@ -0,0 +1,129 @@
using System.Diagnostics.CodeAnalysis;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Enum representing the different types of rich string events.
/// </summary>
public enum RDDialogueToneType
{
/// <summary>
/// Static event type.
/// </summary>
Static,
/// <summary>
/// Flash event type.
/// </summary>
Flash,
/// <summary>
/// Very slow event type.
/// </summary>
VerySlow,
/// <summary>
/// Slow event type.
/// </summary>
Slow,
/// <summary>
/// Normal event type.
/// </summary>
Normal,
/// <summary>
/// Fast event type.
/// </summary>
Fast,
/// <summary>
/// Very fast event type.
/// </summary>
VeryFast,
/// <summary>
/// Very very fast event type.
/// </summary>
VeryVeryFast,
/// <summary>
/// Excited event type.
/// </summary>
Excited,
/// <summary>
/// Shout event type.
/// </summary>
Shout,
/// <summary>
/// Shake event type.
/// </summary>
Shake,
/// <summary>
/// Pause event type.
/// </summary>
Pause,
}
/// <summary>
/// Class representing a rich string event.
/// </summary>
/// <param name="Type">Rich string event type.</param>
/// <param name="Index">Rich string event index.</param>
public record struct RDDialogueTone(RDDialogueToneType Type, int Index)
{
/// <summary>
/// Gets the pause duration for the dialogue event.
/// </summary>
public int? Pause { get; init; }
/// <summary>
/// Serializes the rich string event type to its corresponding string representation.
/// </summary>
/// <returns>A string representation of the rich string event type.</returns>
/// <exception cref="NotImplementedException">Thrown when the event type is not implemented.</exception>
public string Serialize() => "[" + Type switch
{
RDDialogueToneType.Static => "static",
RDDialogueToneType.Flash => "flash",
RDDialogueToneType.VerySlow => "vslow",
RDDialogueToneType.Slow => "slow",
RDDialogueToneType.Normal => "normal",
RDDialogueToneType.Fast => "fast",
RDDialogueToneType.VeryFast => "vfast",
RDDialogueToneType.VeryVeryFast => "vvvfast",
RDDialogueToneType.Excited => "excited",
RDDialogueToneType.Shout => "shout",
RDDialogueToneType.Shake => "shake",
RDDialogueToneType.Pause => Pause?.ToString(),
_ => throw new NotImplementedException(),
} + "]";
/// <summary>
/// Creates a new instance of <see cref="RDDialogueTone"/> based on the provided type and index.
/// </summary>
/// <param name="type">The string representation of the event type.</param>
/// <param name="index">The index of the event.</param>
/// <param name="result">The created <see cref="RDDialogueTone"/> instance if successful, otherwise null.</param>
/// <returns>True if the event was successfully created, otherwise false.</returns>
public static bool Create(string type, int index, [NotNullWhen(true)] out RDDialogueTone? result)
{
RDDialogueToneType? eventType = type switch
{
"static" => RDDialogueToneType.Static,
"flash" => RDDialogueToneType.Flash,
"vslow" => RDDialogueToneType.VerySlow,
"slow" => RDDialogueToneType.Slow,
"normal" => RDDialogueToneType.Normal,
"fast" => RDDialogueToneType.Fast,
"vfast" => RDDialogueToneType.VeryFast,
"vvvfast" => RDDialogueToneType.VeryVeryFast,
"excited" => RDDialogueToneType.Excited,
"shout" => RDDialogueToneType.Shout,
"shake" => RDDialogueToneType.Shake,
_ => null,
};
if (eventType is null)
{
if (int.TryParse(type, out int pause))
{
result = new(RDDialogueToneType.Pause, index) { Pause = pause };
return true;
}
result = null;
return false;
}
result = new(eventType.Value, index);
return true;
}
}
}
@@ -0,0 +1,260 @@
using System.Diagnostics;
using System.Text;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a list of rich text strings.
/// </summary>
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public struct RDLine<TStyle>()
: IRDRichTextLine<TStyle>
where TStyle : IRDRichStringStyle<TStyle>, new()
{
/// <summary>
/// Gets or sets the list of rich text strings.
/// </summary>
private RDPhrase<TStyle>[] texts = [];
/// <summary>
/// The length of the string.
/// </summary>
public readonly int Length => texts.Sum(i => i.Length);
/// <summary>
/// Gets or sets the <see cref="RDLine{RDPhraseStyle}"/> at the specified index.
/// </summary>
/// <param name="index">The index of the <see cref="RDLine{RDPhraseStyle}"/> to get or set.</param>
/// <returns>The <see cref="RDLine{RDPhraseStyle}"/> at the specified index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the index is out of range.</exception>
public RDLine<TStyle> this[Index index]
{
get
{
int i = index.GetOffset(Length);
if (Length <= i)
throw new ArgumentOutOfRangeException(nameof(index));
int ti = 0;
while (texts[ti].Length < i)
{
i -= texts[ti].Length;
ti++;
}
RDLine<TStyle> line = new()
{
texts = [texts[ti][i]]
};
return line;
}
set
{
int i = index.GetOffset(Length);
if (Length <= i)
throw new ArgumentOutOfRangeException(nameof(index));
texts = Concat([this[..i], value, this[(i + 1)..]]).texts;
}
}
/// <summary>
/// Gets or sets the <see cref="RDLine{RDPhraseStyle}"/> within the specified range.
/// </summary>
/// <param name="range">The range of the <see cref="RDLine{RDPhraseStyle}"/> to get or set.</param>
/// <returns>The <see cref="RDLine{RDPhraseStyle}"/> within the specified range.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the range is out of bounds.</exception>
public RDLine<TStyle> this[Range range]
{
get
{
int start = range.Start.GetOffset(Length);
int end = range.End.GetOffset(Length);
if (!(start <= end && end <= Length))
throw new ArgumentOutOfRangeException(nameof(range));
int ti = 0, tstart, tend;
RDPhrase<TStyle>[] strings = [];
while (texts[ti].Length <= start)
{
start -= texts[ti].Length;
end -= texts[ti].Length;
ti++;
}
tstart = ti;
while (texts[ti].Length < end)
{
end -= texts[ti].Length;
ti++;
}
tend = ti;
if (tstart == tend)
strings = [texts[tstart][start..end]];
else
{
for (int i = tstart + 1; i < tend; i++)
strings = [.. strings, texts[i]];
strings = [texts[tstart][start..], .. strings, texts[tend][..end]];
}
RDLine<TStyle> line = new()
{
texts = strings
};
return line;
}
set
{
int start = range.Start.GetOffset(Length);
int end = range.End.GetOffset(Length);
if (!(start < end && end <= Length))
throw new ArgumentOutOfRangeException(nameof(range));
texts = Concat([this[..start], value, this[end..]]).texts;
}
}
/// <inheritdoc/>
public static RDLine<TStyle> Concat(params RDLine<TStyle>[] lines)
{
RDPhrase<TStyle>[] texts = [.. lines[0].texts];
foreach (RDLine<TStyle> line in lines[1..])
{
if (texts[^1].Style == line.texts[0].Style)
{
RDPhrase<TStyle> before = texts[^1], after = line.texts[0];
RDPhrase<TStyle> richString = new(before.Text + after.Text)
{
Style = before.Style,
Events = [.. before.Events, .. after.Events.Select(i => new RDDialogueTone(i.Type, i.Index + before.Length) { Pause = i.Pause })]
};
texts = [.. texts[..^1], richString, .. line.texts[1..]];
}
else
texts = [.. texts, .. line.texts];
}
return new() { texts = texts };
}
/// <summary>
/// Implicitly converts a <see cref="RDPhrase{RDPhraseStyle}"/> to a <see cref="RDLine{RDPhraseStyle}"/>.
/// </summary>
/// <param name="text">The <see cref="RDPhrase{RDPhraseStyle}"/> to convert.</param>
/// <returns>A new <see cref="RDLine{RDPhraseStyle}"/> containing the specified <see cref="RDPhrase{RDPhraseStyle}"/>.</returns>
public static implicit operator RDLine<TStyle>(RDPhrase<TStyle> text) => new() { texts = [text] };
/// <summary>
/// Implicitly converts a <see cref="RDPhrase{RDPhraseStyle}"/> to a <see cref="RDLine{RDPhraseStyle}"/>.
/// </summary>
/// <param name="texts">The <see cref="RDPhrase{RDPhraseStyle}"/> to convert.</param>
/// <returns>A new <see cref="RDLine{RDPhraseStyle}"/> containing the specified <see cref="RDPhrase{RDPhraseStyle}"/>.</returns>
public static implicit operator RDLine<TStyle>(RDPhrase<TStyle>[] texts) => new() { texts = texts };
/// <summary>
/// Implicitly converts a <see cref="string"/> to a <see cref="RDLine{RDPhraseStyle}"/>.
/// </summary>
/// <param name="text">The <see cref="string"/> to convert.</param>
/// <returns>A new <see cref="RDLine{RDPhraseStyle}"/> containing the specified <see cref="string"/>.</returns>
public static implicit operator RDLine<TStyle>(string text) => new() { texts = [new RDPhrase<TStyle>(text)] };
/// <summary>
/// Deserializes a string into an <see cref="RDLine{RDPhraseStyle}"/>.
/// </summary>
/// <param name="text">The string to deserialize.</param>
/// <returns>A new <see cref="RDLine{RDPhraseStyle}"/> containing the deserialized content.</returns>
/// <exception cref="ArgumentNullException">Thrown when the input text is null.</exception>
/// <exception cref="FormatException">Thrown when the input text has an invalid format.</exception>
static public RDLine<TStyle> Deserialize(string text)
{
RDLine<TStyle> line = "";
TStyle style = new();
int start = 0;
while (start < text.Length)
{
TStyle tempStyle = style;
int end = text.IndexOf('<', start);
if (end == -1)
{
line += DeserializeStringPart(text[start..], tempStyle);
break;
}
int start2 = text.IndexOf('>', end);
int end2 = text.IndexOf('<', end + 1);
if (start2 == -1)
break;
if (end2 != -1 && end2 < start2)
{
line += DeserializeStringPart(text[start..end2], tempStyle);
start = end2;
continue;
}
string textpart = text[start..end];
line += DeserializeStringPart(textpart, tempStyle);
string[] keyvalue = text[(end + 1)..start2].Split('=', 2);
if (keyvalue[0].StartsWith('/') && style.ResetProperty(keyvalue[0][1..]))
start = start2 + 1;
else if (style.SetProperty(keyvalue[0], keyvalue.Length == 2 ? keyvalue[1] : "true"))
start = start2 + 1;
else
start = start2 + 1;
}
return line;
}
private static RDPhrase<TStyle> DeserializeStringPart(string text, TStyle style)
{
if (!TStyle.HasPhrase)
return new RDPhrase<TStyle>(text) { Style = style };
int pstart = 0;
RDDialogueTone[] events = [];
while (pstart < text.Length)
{
int pend = text.IndexOf('[', pstart);
if (pend == -1)
{
break;
}
int pstart2 = text.IndexOf(']', pend);
int pend2 = text.IndexOf('[', pend + 1);
if (pstart2 == -1)
break;
if (pend2 != -1 && pend2 < pstart2)
{
pstart = pend2 + 1;
continue;
}
string btag = text[(pend + 1)..pstart2];
if (RDDialogueTone.Create(btag, pend, out RDDialogueTone? e) && e is RDDialogueTone ei)
events = [.. events, ei];
text = text[..pend] + text[(pstart2 + 1)..];
}
return new RDPhrase<TStyle>(text) { Style = style, Events = events };
}
/// <summary>
/// Serializes the current <see cref="RDLine{RDPhraseStyle}"/> instance to a string.
/// </summary>
/// <returns>A string representation of the current <see cref="RDLine{RDPhraseStyle}"/> instance.</returns>
/// <remarks>
/// The serialization process converts the rich text line into a string format, including any styling information.
/// </remarks>
public readonly string Serialize()
{
StringBuilder sb = new();
TStyle style = new();
int ci = 0;
foreach (RDPhrase<TStyle> str in texts)
{
sb.Append(TStyle.GetXmlTag(style, str.Style));
string part = str.Text;
int offset = 0;
foreach (RDDialogueTone e in str.Events)
{
string serialized = e.Serialize();
part = part.Insert(e.Index + offset, serialized);
offset += serialized.Length;
}
sb.Append(part);
ci += str.Length;
style = str.Style;
}
sb.Append(TStyle.GetXmlTag(style, new()));
return sb.ToString();
}
/// <summary>
/// Concatenates two <see cref="RDLine{RDPhraseStyle}"/> instances.
/// </summary>
/// <param name="left">The left <see cref="RDLine{RDPhraseStyle}"/>.</param>
/// <param name="right">The right <see cref="RDLine{RDPhraseStyle}"/>.</param>
/// <returns>A new <see cref="RDLine{RDPhraseStyle}"/> that is the result of concatenating the two specified instances.</returns>
public static RDLine<TStyle> operator +(RDLine<TStyle> left, RDLine<TStyle> right) => Concat([.. left.texts, .. right.texts]);
/// <inheritdoc/>
public override readonly string ToString() => string.Join("", texts);
/// <inheritdoc/>
private readonly string GetDebuggerDisplay() => ToString();
}
}
@@ -0,0 +1,100 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a rich text string with various styling options.
/// </summary>
/// <param name="text">The text content of the rich string.</param>
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public struct RDPhrase<TStyle>(string text)
: IEqualityOperators<RDPhrase<TStyle>, RDPhrase<TStyle>, bool>,
IEquatable<RDPhrase<TStyle>>
where TStyle : IRDRichStringStyle<TStyle>, new()
{
/// <summary>
/// Gets the text content of the rich string.
/// </summary>
public string Text { get; internal set; } = text;
/// <summary>
/// Gets or sets the events associated with the rich string.
/// </summary>
public RDDialogueTone[] Events { get; init; } = [];
/// <summary>
/// Gets the length of the text content.
/// </summary>
/// <value>The number of characters in the text content.</value>
public readonly int Length => Text.Length;
/// <summary>
/// Gets the rich string at the specified index.
/// </summary>
/// <param name="index">The index of the character.</param>
/// <returns>A new <see cref="RDPhrase{TStyle}"/> with the character at the specified index.</returns>
public RDPhrase<TStyle> this[Index index]
{
get
{
return new RDPhrase<TStyle>
{
Text = Text[index].ToString(),
Style = Style,
Events = GetEvents(index.GetOffset(Length), 1)
};
}
}
/// <summary>
/// Gets the rich string within the specified range.
/// </summary>
/// <param name="range">The range of characters.</param>
/// <returns>A new <see cref="RDPhrase{TStyle}"/> with the characters within the specified range.</returns>
public RDPhrase<TStyle> this[Range range]
{
get
{
RDPhrase<TStyle> style = new()
{
Text = Text[range],
Style = Style,
Events = GetEvents(range.Start.GetOffset(Length), range.End.GetOffset(Length) - range.Start.GetOffset(Length))
};
return style;
}
}
private readonly RDDialogueTone[] GetEvents(int start, int length) => Events
.Where(e => e.Index >= start && e.Index < start + length)
.Select(e => new RDDialogueTone(e.Type, e.Index - start))
.ToArray();
/// <summary>
/// Gets a new <see cref="RDPhrase{TStyle}"/> with the same style as the current instance.
/// </summary>
public TStyle Style { get; init; } = new();
/// <summary>
/// Initializes a new instance of the <see cref="RDPhrase{TStyle}"/> struct with an empty text.
/// </summary>
public RDPhrase() : this("") { }
/// <summary>
/// Implicitly converts a string to an <see cref="RDPhrase{TStyle}"/>.
/// </summary>
/// <param name="text">The text to convert.</param>
/// <returns>A new <see cref="RDPhrase{TStyle}"/> instance with the specified text.</returns>
public static implicit operator RDPhrase<TStyle>(string text) => new() { Text = text };
/// <inheritdoc/>
public static bool operator ==(RDPhrase<TStyle> left, RDPhrase<TStyle> right) => left.Text == right.Text && left.Style == right.Style;
/// <inheritdoc/>
public static bool operator !=(RDPhrase<TStyle> left, RDPhrase<TStyle> right) => !(left == right);
/// <inheritdoc/>
public readonly bool Equals(RDPhrase<TStyle> other) => this == other;
/// <inheritdoc/>
public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPhrase<TStyle> && base.Equals(obj);
/// <inheritdoc/>
public override readonly int GetHashCode() => Text.GetHashCode();
/// <inheritdoc/>
public override readonly string ToString() => Text;
/// <inheritdoc/>
private readonly string GetDebuggerDisplay() => ToString();
}
}
@@ -0,0 +1,71 @@
using System.Diagnostics.CodeAnalysis;
namespace RhythmBase.Components.RichText
{
/// <summary>
/// Represents a rich string style.
/// </summary>
public struct RDRichStringStyle : IRDRichStringStyle<RDRichStringStyle>
{
/// <summary>
/// 获取或设置文本的颜色。
/// </summary>
public RDColor? Color { get; set; }
/// <inheritdoc/>
public static bool HasPhrase => false;
/// <inheritdoc/>
public static string GetXmlTag(RDRichStringStyle before, RDRichStringStyle after)
{
string tag = "";
IRDRichStringStyle<RDRichStringStyle>.TryAddTag(ref tag, "color",
before.Color?.TryGetName(out string[] namesbefore) == true
? namesbefore[0].ToLower()
: before.Color?.ToString(before.Color?.A == 255 ? "#RRGGBB" : "#RRGGBBAA"),
after.Color?.TryGetName(out string[] namesafter) == true
? namesafter[0].ToLower()
: after.Color?.ToString(after.Color?.A == 255 ? "#RRGGBB" : "#RRGGBBAA"));
return tag;
}
/// <inheritdoc/>
public readonly bool Equals(RDRichStringStyle other) => this == other;
/// <inheritdoc/>
public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is RDRichStringStyle e && Equals(e);
/// <inheritdoc/>
public bool ResetProperty(string name)
{
switch (name)
{
case "color":
Color = null;
break;
default:
return false;
}
return true;
}
/// <inheritdoc/>
public bool SetProperty(string name, string value)
{
switch (name)
{
case "color":
if (RDColor.TryFromName(value, out RDColor color))
Color = color;
else if (RDColor.TryFromRgba(value, out color))
Color = color;
else
return false;
break;
default:
return false;
}
return true;
}
/// <inheritdoc/>
public static bool operator ==(RDRichStringStyle left, RDRichStringStyle right) => left.Color == right.Color;
/// <inheritdoc/>
public static bool operator !=(RDRichStringStyle left, RDRichStringStyle right) => !(left == right);
/// <inheritdoc/>
public readonly override int GetHashCode() => Color.GetHashCode();
}
}