first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
namespace Nitrox.Launcher.Models.Controls;
/// <summary>
/// A basic Rich Textbox. Supports bold, italic, underline, colors and hyperlinks.
/// </summary>
/// <remarks>
/// Tag legend:<br />
/// [b][/b] - Bold <br />
/// [i][/i] - Italicize <br />
/// [u][/u] - Underline <br />
/// [#colorHex][/#colorHex] - Change text color <br />
/// [Flavor text](example.com) <br />
/// </remarks>
/// <example>
/// [b]Text[/b] => <b>Text</b> <br />
/// [i]Text[/i] => <i>Text</i> <br />
/// [u]Text[/u] => <u>Text</u> <br />
/// [#0000FF]Text[/#0000FF] => Text (with blue foreground) <br />
/// <a href="https://example.com">Flavor text</a> <br />
/// </example>
public partial class RichTextBlock : TextBlock
{
private static readonly TextDecorationCollection underlineTextDecoration = [new() { Location = TextDecorationLocation.Underline }];
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TextProperty)
{
Inlines?.Clear();
ParseTextAndAddInlines(Text ?? "", Inlines);
// If all text was just tags, set Text to empty. Otherwise, it will be displayed as fallback by Avalonia.
if (Inlines?.Count < 1)
{
Text = "";
}
}
}
[GeneratedRegex(@"\[\/?([^]]+)\](?:\(([^\)]*)\))?")]
private static partial Regex TagParserRegex { get; }
public static void ParseTextAndAddInlines(ReadOnlySpan<char> text, InlineCollection inlines)
{
if (inlines == null)
{
return;
}
Regex.ValueMatchEnumerator matchEnumerator = TagParserRegex.EnumerateMatches(text);
if (!matchEnumerator.MoveNext())
{
inlines.Add(new Run(text.ToString()));
return;
}
ValueMatch lastRange = default;
Dictionary<string, Action<Run, string>> activeTags = new(4);
do
{
ValueMatch range = matchEnumerator.Current;
// Handle text in-between previous and current tag.
ReadOnlySpan<char> textPart = text[(lastRange.Index + lastRange.Length)..range.Index];
if (!textPart.IsEmpty)
{
inlines.Add(CreateRunWithTags(textPart.ToString(), activeTags));
}
// Handle current tag (this tracks state of active tags at current text position)
ReadOnlySpan<char> match = text.Slice(range.Index, range.Length);
switch (match)
{
case ['[', '/', ..]:
activeTags.Remove(match[2..^1].ToString());
break;
case "[b]":
activeTags["b"] = static (run, _) => run.FontWeight = FontWeight.Bold;
break;
case "[u]":
activeTags["u"] = static (run, _) => run.TextDecorations = underlineTextDecoration;
break;
case "[i]":
activeTags["i"] = static (run, _) => run.FontStyle = FontStyle.Italic;
break;
case ['[', ..] when match.IndexOf("](", StringComparison.OrdinalIgnoreCase) > -1:
TextBlock textBlock = new();
textBlock.Classes.Add("link");
textBlock.Text = match[1..match.IndexOfAny("]")].ToString();
textBlock.Tag = match[(match.IndexOfAny("(")+1)..match.IndexOfAny(")")].ToString();
inlines.Add(textBlock);
break;
case ['[', '#', ..]:
ReadOnlySpan<char> colorCode = match[1..match.IndexOfAny("]")];
if (!Color.TryParse(colorCode, out Color _))
{
goto default;
}
activeTags[colorCode.ToString()] = static (run, tag) => run.Foreground = new SolidColorBrush(Color.Parse(tag));
break;
default:
// Unknown tag, let's handle as normal text (issue is likely due to input text not knowing about this RichTextBox format)
inlines.Add(CreateRunWithTags(match.ToString(), activeTags));
break;
}
lastRange = range;
} while (matchEnumerator.MoveNext());
// Handle any final text (after the last tag).
ReadOnlySpan<char> lastPart = text[(lastRange.Index + lastRange.Length)..];
if (!lastPart.IsEmpty)
{
inlines.Add(CreateRunWithTags(lastPart.ToString(), activeTags));
}
}
private static Run CreateRunWithTags(string text, Dictionary<string, Action<Run, string>> tags)
{
Run run = new(text);
KeyValuePair<string, Action<Run, string>>? lastColorTag = null;
foreach (KeyValuePair<string, Action<Run, string>> pair in tags)
{
switch (pair.Key)
{
case ['#', ..]:
// Optimization: only the last color needs to be applied for the current run, ignore all others.
lastColorTag = pair;
break;
default:
pair.Value(run, pair.Key);
break;
}
}
lastColorTag?.Value(run, lastColorTag.Value.Key);
return run;
}
protected override Type StyleKeyOverride { get; } = typeof(TextBlock);
}