diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs index 897c014f..42a60d62 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs @@ -1,12 +1,14 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Media; using Snap.Hutao.Control.Extension; using Snap.Hutao.Control.Media; +using Snap.Hutao.Control.Text.Syntax.MiHoYo; using Snap.Hutao.Control.Theme; using Snap.Hutao.Metadata; using Windows.Foundation; @@ -16,23 +18,12 @@ namespace Snap.Hutao.Control.Text; /// /// 专用于呈现描述文本的文本块 -/// Some part of this file came from: -/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs /// [HighQuality] [DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))] [DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))] internal sealed partial class DescriptionTextBlock : ContentControl { - private static readonly int RgbaColorTagFullLength = "".Length; - private static readonly int RgbaColorTagLeftLength = "".Length; - - private static readonly int RgbColorTagFullLength = "".Length; - private static readonly int RgbColorTagLeftLength = "".Length; - - private static readonly int ItalicTagFullLength = "".Length; - private static readonly int ItalicTagLeftLength = "".Length; - private readonly TypedEventHandler actualThemeChangedEventHandler; /// @@ -59,7 +50,7 @@ internal sealed partial class DescriptionTextBlock : ContentControl try { - UpdateDescription(textBlock, description); + UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue))); } catch (Exception ex) { @@ -73,85 +64,62 @@ internal sealed partial class DescriptionTextBlock : ContentControl textBlock.Style = (Style)e.NewValue; } - private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan description) + private static void UpdateDescription(TextBlock textBlock, MiHoYoSyntaxTree syntaxTree) { textBlock.Inlines.Clear(); + AppendNode(textBlock, textBlock.Inlines, syntaxTree.Root); + } - int last = 0; - for (int i = 0; i < description.Length;) + private static void AppendNode(TextBlock textBlock, InlineCollection inlines, MiHoYoSyntaxNode node) + { + switch (node.Kind) { - // newline - if (description[i..].StartsWith(@"\n")) - { - AppendText(textBlock, description[last..i]); - AppendLineBreak(textBlock); - i += 2; - last = i; - } - - // color tag - else if (description[i..].StartsWith("')) + case MiHoYoSyntaxKind.Root: + foreach (MiHoYoSyntaxNode child in ((MiHoYoRootSyntax)node).Children) { - case 16: // RgbaColorTag - { - AppendText(textBlock, description[last..i]); - Rgba32 color = new(description.Slice(i + 8, 8).ToString()); - int length = description[(i + RgbaColorTagLeftLength)..].IndexOf('<'); - AppendColorText(textBlock, description.Slice(i + RgbaColorTagLeftLength, length), color); - - i += length + RgbaColorTagFullLength; - last = i; - break; - } - - case 14: // RgbColorTag - { - AppendText(textBlock, description[last..i]); - Rgba32 color = new(description.Slice(i + 8, 6).ToString()); - int length = description[(i + RgbColorTagLeftLength)..].IndexOf('<'); - AppendColorText(textBlock, description.Slice(i + RgbColorTagLeftLength, length), color); - - i += length + RgbColorTagFullLength; - last = i; - break; - } + AppendNode(textBlock, inlines, child); } - } - // italic - else if (description[i..].StartsWith(" slice) + private static void AppendLine(TextBlock textBlock, InlineCollection inlines, MiHoYoLineSyntax line) { - text.Inlines.Add(new Run { Text = slice.ToString() }); + foreach (MiHoYoSyntaxNode node in line.Children) + { + AppendNode(textBlock, inlines, node); + } + + if (line.HasTailingNewLine) + { + inlines.Add(new LineBreak()); + } } - private static void AppendColorText(TextBlock text, in ReadOnlySpan slice, Rgba32 color) + private static void AppendPlainText(TextBlock textBlock, InlineCollection inlines, MiHoYoPlainTextSyntax plainText) { + // PlainText doesn't have children + inlines.Add(new Run { Text = plainText.Span.ToString() }); + } + + private static void AppendColorText(TextBlock textBlock, InlineCollection inlines, MiHoYoColorTextSyntax colorText) + { + Rgba32 color = new(colorText.ColorSpan.ToString()); Color targetColor; - if (ThemeHelper.IsDarkMode(text.ActualTheme)) + if (ThemeHelper.IsDarkMode(textBlock.ActualTheme)) { targetColor = color; } @@ -163,30 +131,55 @@ internal sealed partial class DescriptionTextBlock : ContentControl targetColor = Rgba32.FromHsl(hsl); } - text.Inlines.Add(new Run + if (colorText.Children.Count > 0) { - Text = slice.ToString(), - Foreground = new SolidColorBrush(targetColor), - }); + Span span = new() + { + Foreground = new SolidColorBrush(targetColor), + }; + + foreach (MiHoYoSyntaxNode child in colorText.Children) + { + AppendNode(textBlock, span.Inlines, child); + } + } + else + { + inlines.Add(new Run + { + Text = colorText.ContentSpan.ToString(), + Foreground = new SolidColorBrush(targetColor), + }); + } } - private static void AppendItalicText(TextBlock text, in ReadOnlySpan slice) + private static void AppendItalicText(TextBlock textBlock, InlineCollection inlines, MiHoYoItalicTextSyntax italicText) { - text.Inlines.Add(new Run + if (italicText.Children.Count > 0) { - Text = slice.ToString(), - FontStyle = Windows.UI.Text.FontStyle.Italic, - }); - } + Span span = new() + { + FontStyle = Windows.UI.Text.FontStyle.Italic, + }; - private static void AppendLineBreak(TextBlock text) - { - text.Inlines.Add(new LineBreak()); + foreach (MiHoYoSyntaxNode child in italicText.Children) + { + AppendNode(textBlock, span.Inlines, child); + } + } + else + { + inlines.Add(new Run + { + Text = italicText.ContentSpan.ToString(), + FontStyle = Windows.UI.Text.FontStyle.Italic, + }); + } } private void OnActualThemeChanged(FrameworkElement sender, object args) { // Simply re-apply texts - UpdateDescription((TextBlock)Content, Description); + UpdateDescription((TextBlock)Content, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle(Description))); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs new file mode 100644 index 00000000..ccaa8b34 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal enum MiHoYoColorKind +{ + None, + Rgba, + Rgb, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs new file mode 100644 index 00000000..c5662232 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax +{ + public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, int start, int end) + : base(MiHoYoSyntaxKind.ColorText, text, start, end) + { + ColorKind = colorKind; + } + + public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, TextPosition position) + : base(MiHoYoSyntaxKind.ColorText, text, position) + { + ColorKind = colorKind; + } + + public MiHoYoColorKind ColorKind { get; } + + public override TextPosition ContentPosition + { + get + { + return ColorKind switch + { + MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8), + MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8), + _ => throw Must.NeverHappen(), + }; + } + } + + public TextPosition ColorPosition + { + get + { + return ColorKind switch + { + MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16), + MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14), + _ => throw Must.NeverHappen(), + }; + } + } + + public ReadOnlySpan ColorSpan { get => Text.AsSpan()[ColorPosition.Start..ColorPosition.End]; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs new file mode 100644 index 00000000..e02fdb5c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoItalicTextSyntax : MiHoYoXmlElementSyntax +{ + public MiHoYoItalicTextSyntax(string text, int start, int end) + : base(MiHoYoSyntaxKind.ItalicText, text, start, end) + { + } + + public MiHoYoItalicTextSyntax(string text, TextPosition position) + : base(MiHoYoSyntaxKind.ItalicText, text, position) + { + } + + public override TextPosition ContentPosition { get => new(Position.Start + 3, Position.End - 4); } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs new file mode 100644 index 00000000..43711298 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoLineSyntax : MiHoYoSyntaxNode +{ + public MiHoYoLineSyntax(bool hasTailingNewLine, string text, int start, int end) + : base(MiHoYoSyntaxKind.Line, text, start, end) + { + HasTailingNewLine = hasTailingNewLine; + } + + public bool HasTailingNewLine { get; } + + public TextPosition TextPosition { get => HasTailingNewLine ? new(Position.Start, Position.Length - 1) : Position; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs new file mode 100644 index 00000000..7bf707d8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoPlainTextSyntax : MiHoYoSyntaxNode +{ + public MiHoYoPlainTextSyntax(string text, int start, int end) + : base(MiHoYoSyntaxKind.PlainText, text, start, end) + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs new file mode 100644 index 00000000..73c003e5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoRootSyntax : MiHoYoSyntaxNode +{ + public MiHoYoRootSyntax(string text, int start, int end) + : base(MiHoYoSyntaxKind.Root, text, start, end) + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs new file mode 100644 index 00000000..d844920c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal enum MiHoYoSyntaxKind +{ + None, + Root, + Line, + PlainText, + ColorText, + ItalicText, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs new file mode 100644 index 00000000..297afd66 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal abstract class MiHoYoSyntaxNode : SyntaxNode +{ + public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, int start, int end) + : base(kind, text, start, end) + { + } + + public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, TextPosition position) + : base(kind, text, position) + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs new file mode 100644 index 00000000..79cb2296 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs @@ -0,0 +1,163 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal sealed class MiHoYoSyntaxTree +{ + public MiHoYoSyntaxNode Root { get; set; } = default!; + + public string Text { get; set; } = default!; + + public static MiHoYoSyntaxTree Parse(string text) + { + MiHoYoRootSyntax root = new(text, 0, text.Length); + ParseLines(text, root); + + MiHoYoSyntaxTree tree = new() + { + Text = text, + Root = root, + }; + + return tree; + } + + private static void ParseLines(string text, MiHoYoRootSyntax syntax) + { + ReadOnlySpan textSpan = text.AsSpan(); + int previousProcessedIndexOfText = 0; + + while (true) + { + int newLineIndexAtSlicedText = textSpan[previousProcessedIndexOfText..].IndexOf('\n'); + + if (newLineIndexAtSlicedText < 0) + { + MiHoYoLineSyntax line = new(false, text, previousProcessedIndexOfText, textSpan.Length); + ParseComponents(text, line); + syntax.Children.Add(line); + break; + } + + MiHoYoLineSyntax lineWithBreaking = new(true, text, previousProcessedIndexOfText, previousProcessedIndexOfText + newLineIndexAtSlicedText + 1); + ParseComponents(text, lineWithBreaking); + syntax.Children.Add(lineWithBreaking); + + previousProcessedIndexOfText = lineWithBreaking.Position.End; + } + } + + private static void ParseComponents(string text, MiHoYoSyntaxNode syntax) + { + TextPosition contentPosition = syntax switch + { + MiHoYoXmlElementSyntax xmlSyntax => xmlSyntax.ContentPosition, + MiHoYoLineSyntax lineSyntax => lineSyntax.TextPosition, + _ => syntax.Position, + }; + ReadOnlySpan contentSpan = text.AsSpan().Slice(contentPosition.Start, contentPosition.Length); + + int previousProcessedIndexOfContent = 0; + while (true) + { + int fullXmlOpeningIndexOfContent = contentSpan[previousProcessedIndexOfContent..].IndexOf('<'); + + // End of content + if (fullXmlOpeningIndexOfContent < 0) + { + MiHoYoPlainTextSyntax plainText = new(text, contentPosition.Start + previousProcessedIndexOfContent, contentPosition.End); + syntax.Children.Add(plainText); + break; + } + + // We have plain text between xml elements + if (previousProcessedIndexOfContent < fullXmlOpeningIndexOfContent) + { + MiHoYoPlainTextSyntax plainText = new(text, contentPosition.Start + previousProcessedIndexOfContent, contentPosition.End); + syntax.Children.Add(plainText); + } + + // Peek the next character after '<' + switch (contentSpan[previousProcessedIndexOfContent + fullXmlOpeningIndexOfContent + 1]) + { + case 'c': + { + // + // + int colorTagClosingEndOfSlicedContent = IndexOfClosingEnd(contentSpan[fullXmlOpeningIndexOfContent..], out int colorTagLeftClosingEndOfSlicedContent); + + MiHoYoColorKind colorKind = colorTagLeftClosingEndOfSlicedContent switch + { + 17 => MiHoYoColorKind.Rgba, + 15 => MiHoYoColorKind.Rgb, + _ => throw Must.NeverHappen(), + }; + + TextPosition positionOfColorElement = new(0, colorTagClosingEndOfSlicedContent); + TextPosition positionAtContent = positionOfColorElement.Add(fullXmlOpeningIndexOfContent); + TextPosition positionAtText = positionAtContent.Add(contentPosition.Start + previousProcessedIndexOfContent); + + MiHoYoColorTextSyntax colorText = new(colorKind, text, positionAtText); + ParseComponents(text, colorText); + syntax.Children.Add(colorText); + previousProcessedIndexOfContent = positionAtContent.End; + break; + } + + case 'i': + { + // sometext 14 + int italicTagClosingEndOfSlicedContent = IndexOfClosingEnd(contentSpan[fullXmlOpeningIndexOfContent..], out _); + + TextPosition positionOfItalicElement = new(0, italicTagClosingEndOfSlicedContent); + TextPosition positionAtContent = positionOfItalicElement.Add(fullXmlOpeningIndexOfContent); + TextPosition positionAtText = positionAtContent.Add(contentPosition.Start + previousProcessedIndexOfContent); + + MiHoYoItalicTextSyntax italicText = new(text, positionAtText); + ParseComponents(text, italicText); + syntax.Children.Add(italicText); + previousProcessedIndexOfContent = positionAtContent.End; + break; + } + } + } + } + + private static int IndexOfClosingEnd(in ReadOnlySpan span, out int leftClosingEnd) + { + leftClosingEnd = 0; + + int openingCount = 0; + int closingCount = 0; + + int current = 0; + + // Considering text1text2text3 + // Considering text1text2text3 + while (true) + { + int leftMarkIndex = span[current..].IndexOf('<'); + if (span[current..][leftMarkIndex + 1] is '/') + { + closingCount++; + } + else + { + openingCount++; + } + + current += span[current..].IndexOf('>') + 1; + + if (openingCount is 1 && closingCount is 0) + { + leftClosingEnd = current; + } + + if (openingCount == closingCount) + { + return current; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs new file mode 100644 index 00000000..8907bb49 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax.MiHoYo; + +internal abstract class MiHoYoXmlElementSyntax : MiHoYoSyntaxNode +{ + public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, int start, int end) + : base(kind, text, start, end) + { + } + + public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, TextPosition position) + : base(kind, text, position) + { + } + + public abstract TextPosition ContentPosition { get; } + + public ReadOnlySpan ContentSpan { get => Text.AsSpan(ContentPosition.Start, ContentPosition.Length); } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs new file mode 100644 index 00000000..0ed91eea --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Control.Text.Syntax; + +internal abstract class SyntaxNode + where TSelf : SyntaxNode + where TKind : struct, Enum +{ + public SyntaxNode(TKind kind, string text, int start, int end) + { + Kind = kind; + Text = text; + Position = new(start, end); + } + + public SyntaxNode(TKind kind, string text, TextPosition position) + { + Kind = kind; + Text = text; + Position = position; + } + + public TKind Kind { get; protected set; } + + public List Children { get; } = []; + + public TextPosition Position { get; protected set; } + + public ReadOnlySpan Span { get => Text.AsSpan().Slice(Position.Start, Position.Length); } + + protected string Text { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs new file mode 100644 index 00000000..89bf97c9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; + +namespace Snap.Hutao.Control.Text.Syntax; + +[DebuggerDisplay("[{Start}..{End}]")] +internal readonly struct TextPosition +{ + public readonly int Start; + public readonly int End; + + public TextPosition(int start, int end) + { + Start = start; + End = end; + } + + public readonly int Length + { + get => End - Start; + } + + public TextPosition Add(int offset) + { + return new(Start + offset, End + offset); + } +} \ No newline at end of file