mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Implement SeString renderer (#1977)
Does not have contextual evaluation. Only styling payloads are handled for (relative) simplicity.
This commit is contained in:
parent
1c0ad61335
commit
844b04faad
24 changed files with 14665 additions and 21 deletions
|
|
@ -114,6 +114,13 @@
|
|||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\EastAsianWidth.txt" LogicalName="EastAsianWidth.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\DerivedGeneralCategory.txt" LogicalName="DerivedGeneralCategory.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\emoji-data.txt" LogicalName="emoji-data.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
|
||||
<ItemGroup>
|
||||
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />
|
||||
|
|
|
|||
|
|
@ -121,7 +121,10 @@ internal sealed class GameConfig : IInternalDisposableService, IGameConfig
|
|||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out PadButtonValue value) => this.System.TryGetStringAsEnum(option.GetName(), out value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value);
|
||||
|
||||
|
|
@ -346,7 +349,11 @@ internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
|
|||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
|
||||
=> this.gameConfigService.TryGet(option, out properties);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out PadButtonValue value)
|
||||
=> this.gameConfigService.TryGet(option, out value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(UiConfigOption option, out bool value)
|
||||
=> this.gameConfigService.TryGet(option, out value);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -357,6 +358,40 @@ public class GameConfigSection
|
|||
return value;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to get a string config value as an enum value.</summary>
|
||||
/// <param name="name">Name of the config option.</param>
|
||||
/// <param name="value">The returned value of the config option.</param>
|
||||
/// <typeparam name="T">Type of the enum. Name of each enum fields are compared against.</typeparam>
|
||||
/// <returns>A value representing the success.</returns>
|
||||
public unsafe bool TryGetStringAsEnum<T>(string name, out T value) where T : struct, Enum
|
||||
{
|
||||
value = default;
|
||||
if (!this.TryGetIndex(name, out var index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.TryGetEntry(index, out var entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry->Type != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry->Value.String == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var n8 = entry->Value.String->AsSpan();
|
||||
Span<char> n16 = stackalloc char[Encoding.UTF8.GetCharCount(n8)];
|
||||
Encoding.UTF8.GetChars(n8, n16);
|
||||
return Enum.TryParse(n16, out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a string config option.
|
||||
/// Note: Not all config options will be be immediately reflected in the game.
|
||||
|
|
|
|||
85
Dalamud/Game/Config/PadButtonValue.cs
Normal file
85
Dalamud/Game/Config/PadButtonValue.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
namespace Dalamud.Game.Config;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable IdentifierTypo
|
||||
// ReSharper disable CommentTypo
|
||||
|
||||
/// <summary>Valid values for PadButton options under <see cref="SystemConfigOption"/>.</summary>
|
||||
/// <remarks>Names are the valid part. Enum values are exclusively for use with current Dalamud version.</remarks>
|
||||
public enum PadButtonValue
|
||||
{
|
||||
/// <summary>Auto-run.</summary>
|
||||
Autorun_Support,
|
||||
|
||||
/// <summary>Change Hotbar Set.</summary>
|
||||
Hotbar_Set_Change,
|
||||
|
||||
/// <summary>Highlight Left Hotbar.</summary>
|
||||
XHB_Left_Start,
|
||||
|
||||
/// <summary>Highlight Right Hotbar.</summary>
|
||||
XHB_Right_Start,
|
||||
|
||||
/// <summary>Not directly referenced by Gamepad button customization window.</summary>
|
||||
Cursor_Operation,
|
||||
|
||||
/// <summary>Draw Weapon/Lock On.</summary>
|
||||
Lockon_and_Sword,
|
||||
|
||||
/// <summary>Sit/Lock On.</summary>
|
||||
Lockon_and_Sit,
|
||||
|
||||
/// <summary>Change Camera.</summary>
|
||||
Camera_Modechange,
|
||||
|
||||
/// <summary>Reset Camera Position.</summary>
|
||||
Camera_Reset,
|
||||
|
||||
/// <summary>Draw/Sheathe Weapon.</summary>
|
||||
Drawn_Sword,
|
||||
|
||||
/// <summary>Lock On.</summary>
|
||||
Camera_Lockononly,
|
||||
|
||||
/// <summary>Face Target.</summary>
|
||||
FaceTarget,
|
||||
|
||||
/// <summary>Assist Target.</summary>
|
||||
AssistTarget,
|
||||
|
||||
/// <summary>Face Camera.</summary>
|
||||
LookCamera,
|
||||
|
||||
/// <summary>Execute Macro #98 (Exclusive).</summary>
|
||||
Macro98,
|
||||
|
||||
/// <summary>Execute Macro #99 (Exclusive).</summary>
|
||||
Macro99,
|
||||
|
||||
/// <summary>Not Assigned.</summary>
|
||||
Notset,
|
||||
|
||||
/// <summary>Jump/Cancel Casting.</summary>
|
||||
Jump,
|
||||
|
||||
/// <summary>Select Target/Confirm.</summary>
|
||||
Accept,
|
||||
|
||||
/// <summary>Cancel.</summary>
|
||||
Cancel,
|
||||
|
||||
/// <summary>Open Map/Subcommands.</summary>
|
||||
Map_Sub,
|
||||
|
||||
/// <summary>Open Main Menu.</summary>
|
||||
MainCommand,
|
||||
|
||||
/// <summary>Select HUD.</summary>
|
||||
HUD_Select,
|
||||
|
||||
/// <summary>Move Character.</summary>
|
||||
Move_Operation,
|
||||
|
||||
/// <summary>Move Camera.</summary>
|
||||
Camera_Operation,
|
||||
}
|
||||
149
Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
Normal file
149
Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Lumina.Data;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
|
||||
/// <summary>Reference member view of a .gfd file data.</summary>
|
||||
internal sealed unsafe class GfdFile : FileResource
|
||||
{
|
||||
/// <summary>Gets or sets the file header.</summary>
|
||||
public GfdHeader Header { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the entries.</summary>
|
||||
public GfdEntry[] Entries { get; set; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void LoadFile()
|
||||
{
|
||||
if (this.DataSpan.Length < sizeof(GfdHeader))
|
||||
throw new InvalidDataException($"Not enough space for a {nameof(GfdHeader)}");
|
||||
if (this.DataSpan.Length < sizeof(GfdHeader) + (this.Header.Count * sizeof(GfdEntry)))
|
||||
throw new InvalidDataException($"Not enough space for all the {nameof(GfdEntry)}");
|
||||
|
||||
this.Header = MemoryMarshal.AsRef<GfdHeader>(this.DataSpan);
|
||||
this.Entries = MemoryMarshal.Cast<byte, GfdEntry>(this.DataSpan[sizeof(GfdHeader)..]).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Attempts to get an entry.</summary>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <param name="entry">The entry.</param>
|
||||
/// <param name="followRedirect">Whether to follow redirects.</param>
|
||||
/// <returns><c>true</c> if found.</returns>
|
||||
public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true)
|
||||
{
|
||||
if (iconId == 0)
|
||||
{
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var entries = this.Entries;
|
||||
if (iconId <= this.Entries.Length && entries[(int)(iconId - 1)].Id == iconId)
|
||||
{
|
||||
if (iconId <= entries.Length)
|
||||
{
|
||||
entry = entries[(int)(iconId - 1)];
|
||||
return !entry.IsEmpty;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var lo = 0;
|
||||
var hi = entries.Length;
|
||||
while (lo <= hi)
|
||||
{
|
||||
var i = lo + ((hi - lo) >> 1);
|
||||
if (entries[i].Id == iconId)
|
||||
{
|
||||
if (followRedirect && entries[i].Redirect != 0)
|
||||
{
|
||||
iconId = entries[i].Redirect;
|
||||
lo = 0;
|
||||
hi = entries.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
entry = entries[i];
|
||||
return !entry.IsEmpty;
|
||||
}
|
||||
|
||||
if (entries[i].Id < iconId)
|
||||
lo = i + 1;
|
||||
else
|
||||
hi = i - 1;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Header of a .gfd file.</summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct GfdHeader
|
||||
{
|
||||
/// <summary>Signature: "gftd0100".</summary>
|
||||
public fixed byte Signature[8];
|
||||
|
||||
/// <summary>Number of entries.</summary>
|
||||
public int Count;
|
||||
|
||||
/// <summary>Unused/unknown.</summary>
|
||||
public fixed byte Padding[4];
|
||||
}
|
||||
|
||||
/// <summary>An entry of a .gfd file.</summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
|
||||
public struct GfdEntry
|
||||
{
|
||||
/// <summary>ID of the entry.</summary>
|
||||
public ushort Id;
|
||||
|
||||
/// <summary>The left offset of the entry.</summary>
|
||||
public ushort Left;
|
||||
|
||||
/// <summary>The top offset of the entry.</summary>
|
||||
public ushort Top;
|
||||
|
||||
/// <summary>The width of the entry.</summary>
|
||||
public ushort Width;
|
||||
|
||||
/// <summary>The height of the entry.</summary>
|
||||
public ushort Height;
|
||||
|
||||
/// <summary>Unknown/unused.</summary>
|
||||
public ushort Unk0A;
|
||||
|
||||
/// <summary>The redirected entry, maybe.</summary>
|
||||
public ushort Redirect;
|
||||
|
||||
/// <summary>Unknown/unused.</summary>
|
||||
public ushort Unk0E;
|
||||
|
||||
/// <summary>Gets a value indicating whether this entry is effectively empty.</summary>
|
||||
public bool IsEmpty => this.Width == 0 || this.Height == 0;
|
||||
|
||||
/// <summary>Gets or sets the size of this entry.</summary>
|
||||
public Vector2 Size
|
||||
{
|
||||
get => new(this.Width, this.Height);
|
||||
set => (this.Width, this.Height) = (checked((ushort)value.X), checked((ushort)value.Y));
|
||||
}
|
||||
|
||||
/// <summary>Gets the UV0 of this entry.</summary>
|
||||
public Vector2 Uv0 => new(this.Left / 512f, this.Top / 1024f);
|
||||
|
||||
/// <summary>Gets the UV1 of this entry.</summary>
|
||||
public Vector2 Uv1 => new((this.Left + this.Width) / 512f, (this.Top + this.Height) / 1024f);
|
||||
|
||||
/// <summary>Gets the UV0 of the HQ version of this entry.</summary>
|
||||
public Vector2 HqUv0 => new(this.Left / 256f, (this.Top + 170.5f) / 512f);
|
||||
|
||||
/// <summary>Gets the UV1 of the HQ version of this entry.</summary>
|
||||
public Vector2 HqUv1 => new((this.Left + this.Width) / 256f, (this.Top + this.Height + 170.5f) / 512f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,693 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using BitFaster.Caching.Lru;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Config;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets2;
|
||||
using Lumina.Text.Expressions;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using static Dalamud.Game.Text.SeStringHandling.BitmapFontIcon;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
|
||||
/// <summary>Draws SeString.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class SeStringRenderer : IInternalDisposableService
|
||||
{
|
||||
private const int ChannelShadow = 0;
|
||||
private const int ChannelEdge = 1;
|
||||
private const int ChannelFore = 2;
|
||||
private const int ChannelCount = 3;
|
||||
|
||||
private const char SoftHyphen = '\u00AD';
|
||||
private const char ObjectReplacementCharacter = '\uFFFC';
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfig = Service<GameConfig>.Get();
|
||||
|
||||
private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024);
|
||||
|
||||
private readonly GfdFile gfd;
|
||||
private readonly uint[] colorTypes;
|
||||
private readonly uint[] edgeColorTypes;
|
||||
|
||||
private readonly List<TextFragment> words = [];
|
||||
|
||||
private readonly List<uint> colorStack = [];
|
||||
private readonly List<uint> edgeColorStack = [];
|
||||
private readonly List<uint> shadowColorStack = [];
|
||||
private bool bold;
|
||||
private bool italic;
|
||||
private Vector2 edge;
|
||||
private Vector2 shadow;
|
||||
|
||||
private ImDrawListSplitterPtr splitter = new(ImGuiNative.ImDrawListSplitter_ImDrawListSplitter());
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private SeStringRenderer(DataManager dm)
|
||||
{
|
||||
var uiColor = dm.Excel.GetSheet<UIColor>()!;
|
||||
var maxId = 0;
|
||||
foreach (var row in uiColor)
|
||||
maxId = (int)Math.Max(row.RowId, maxId);
|
||||
|
||||
this.colorTypes = new uint[maxId + 1];
|
||||
this.edgeColorTypes = new uint[maxId + 1];
|
||||
foreach (var row in uiColor)
|
||||
{
|
||||
this.colorTypes[row.RowId] = BgraToRgba((row.UIForeground >> 8) | (row.UIForeground << 24));
|
||||
this.edgeColorTypes[row.RowId] = BgraToRgba((row.UIGlow >> 8) | (row.UIGlow << 24));
|
||||
}
|
||||
|
||||
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
|
||||
|
||||
return;
|
||||
|
||||
static uint BgraToRgba(uint x)
|
||||
{
|
||||
var buf = (byte*)&x;
|
||||
(buf[0], buf[2]) = (buf[2], buf[0]);
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
|
||||
~SeStringRenderer() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <summary>Creates and caches a SeString from a text macro representation, and then draws it.</summary>
|
||||
/// <param name="text">SeString text macro representation.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
public void CompileAndDrawWrapped(string text, float wrapWidth = 0)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
this.DrawWrapped(
|
||||
this.cache.GetOrAdd(
|
||||
text,
|
||||
static text =>
|
||||
{
|
||||
var outstr = default(Utf8String);
|
||||
outstr.Ctor();
|
||||
RaptureTextModule.Instance()->MacroEncoder.EncodeString(&outstr, text.ReplaceLineEndings("<br>"));
|
||||
var res = new ReadOnlySeString(outstr.AsSpan().ToArray());
|
||||
outstr.Dtor();
|
||||
return res;
|
||||
}).AsSpan(),
|
||||
wrapWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="DrawWrapped(ReadOnlySeStringSpan, float)"/>
|
||||
public void DrawWrapped(in Utf8String utf8String, float wrapWidth = 0) =>
|
||||
this.DrawWrapped(utf8String.AsSpan(), wrapWidth);
|
||||
|
||||
/// <summary>Draws a SeString.</summary>
|
||||
/// <param name="sss">SeString to draw.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
public void DrawWrapped(ReadOnlySeStringSpan sss, float wrapWidth = 0)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (wrapWidth <= 0)
|
||||
wrapWidth = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
this.words.Clear();
|
||||
this.colorStack.Clear();
|
||||
this.edgeColorStack.Clear();
|
||||
this.shadowColorStack.Clear();
|
||||
|
||||
this.colorStack.Add(ImGui.GetColorU32(ImGuiCol.Text));
|
||||
this.edgeColorStack.Add(0);
|
||||
this.shadowColorStack.Add(0);
|
||||
this.bold = this.italic = false;
|
||||
this.edge = Vector2.One;
|
||||
this.shadow = Vector2.Zero;
|
||||
|
||||
var state = new DrawState(
|
||||
sss,
|
||||
ImGui.GetWindowDrawList(),
|
||||
this.splitter,
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
ImGui.GetCursorScreenPos());
|
||||
this.CreateTextFragments(ref state, wrapWidth);
|
||||
|
||||
var size = Vector2.Zero;
|
||||
for (var i = 0; i < this.words.Count; i++)
|
||||
{
|
||||
var word = this.words[i];
|
||||
this.DrawWord(
|
||||
ref state,
|
||||
word.Offset,
|
||||
state.Raw.Data[word.From..word.To],
|
||||
i == 0
|
||||
? '\0'
|
||||
: this.words[i - 1].IsSoftHyphenVisible
|
||||
? this.words[i - 1].LastRuneRepr
|
||||
: this.words[i - 1].LastRuneRepr2);
|
||||
|
||||
if (word.IsSoftHyphenVisible && i > 0)
|
||||
{
|
||||
this.DrawWord(
|
||||
ref state,
|
||||
word.Offset + new Vector2(word.AdvanceWidthWithoutLastRune, 0),
|
||||
"-"u8,
|
||||
this.words[i - 1].LastRuneRepr);
|
||||
}
|
||||
|
||||
size = Vector2.Max(size, word.Offset + new Vector2(word.VisibleWidth, state.FontSize));
|
||||
}
|
||||
|
||||
state.Splitter.Merge(state.DrawList);
|
||||
|
||||
ImGui.Dummy(size);
|
||||
}
|
||||
|
||||
/// <summary>Gets the printable char for the given char, or null(\0) if it should not be handled at all.</summary>
|
||||
/// <param name="c">Character to determine.</param>
|
||||
/// <returns>Character to print, or null(\0) if none.</returns>
|
||||
private static Rune? ToPrintableRune(int c) => c switch
|
||||
{
|
||||
char.MaxValue => null,
|
||||
SoftHyphen => new('-'),
|
||||
_ when UnicodeData.LineBreak[c]
|
||||
is UnicodeLineBreakClass.BK
|
||||
or UnicodeLineBreakClass.CR
|
||||
or UnicodeLineBreakClass.LF
|
||||
or UnicodeLineBreakClass.NL => new(0),
|
||||
_ => new(c),
|
||||
};
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (this.splitter.NativePtr is not null)
|
||||
this.splitter.Destroy();
|
||||
this.splitter = default;
|
||||
}
|
||||
|
||||
private void CreateTextFragments(ref DrawState state, float wrapWidth)
|
||||
{
|
||||
var prev = 0;
|
||||
var runningOffset = Vector2.Zero;
|
||||
var runningWidth = 0f;
|
||||
foreach (var (curr, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
var fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset);
|
||||
var nextRunningWidth = Math.Max(runningWidth, runningOffset.X + fragment.VisibleWidth);
|
||||
if (nextRunningWidth <= wrapWidth)
|
||||
{
|
||||
// New fragment fits in the current line.
|
||||
if (this.words.Count > 0)
|
||||
{
|
||||
char lastFragmentEnd;
|
||||
if (this.words[^1].EndsWithSoftHyphen)
|
||||
{
|
||||
runningOffset.X += this.words[^1].AdvanceWidthWithoutLastRune - this.words[^1].AdvanceWidth;
|
||||
lastFragmentEnd = this.words[^1].LastRuneRepr;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastFragmentEnd = this.words[^1].LastRuneRepr2;
|
||||
}
|
||||
|
||||
runningOffset.X += MathF.Round(
|
||||
state.Font.GetDistanceAdjustmentForPair(lastFragmentEnd, fragment.FirstRuneRepr) *
|
||||
state.FontSizeScale);
|
||||
fragment = fragment with { Offset = runningOffset };
|
||||
}
|
||||
|
||||
this.words.Add(fragment);
|
||||
runningWidth = nextRunningWidth;
|
||||
runningOffset.X += fragment.AdvanceWidth;
|
||||
prev = curr;
|
||||
}
|
||||
else if (fragment.VisibleWidth <= wrapWidth)
|
||||
{
|
||||
// New fragment does not fit in the current line, but it will fit in the next line.
|
||||
// Implicit conditions: runningWidth > 0, this.words.Count > 0
|
||||
runningWidth = fragment.VisibleWidth;
|
||||
runningOffset.X = fragment.AdvanceWidth;
|
||||
runningOffset.Y += state.FontSize;
|
||||
prev = curr;
|
||||
this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true };
|
||||
this.words.Add(fragment with { Offset = runningOffset with { X = 0 } });
|
||||
}
|
||||
else
|
||||
{
|
||||
// New fragment does not fit in the given width, and it needs to be broken down.
|
||||
while (prev < curr)
|
||||
{
|
||||
if (runningOffset.X > 0)
|
||||
{
|
||||
runningOffset.X = 0;
|
||||
runningOffset.Y += state.FontSize;
|
||||
}
|
||||
|
||||
fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset, wrapWidth);
|
||||
runningWidth = fragment.VisibleWidth;
|
||||
runningOffset.X = fragment.AdvanceWidth;
|
||||
prev = fragment.To;
|
||||
if (this.words.Count > 0)
|
||||
this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true };
|
||||
this.words.Add(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment.MandatoryBreakAfter)
|
||||
{
|
||||
runningOffset.X = runningWidth = 0;
|
||||
runningOffset.Y += state.FontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan<byte> span, char lastRuneRepr)
|
||||
{
|
||||
var gfdTextureSrv =
|
||||
(nint)UIModule.Instance()->GetRaptureAtkModule()->AtkModule.AtkFontManager.Gfd->Texture->
|
||||
D3D11ShaderResourceView;
|
||||
var x = 0f;
|
||||
var width = 0f;
|
||||
foreach (var c in UtfEnumerator.From(span, UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
if (c.IsSeStringPayload)
|
||||
{
|
||||
var enu = new ReadOnlySeStringSpan(span[c.ByteOffset..]).GetEnumerator();
|
||||
if (!enu.MoveNext())
|
||||
continue;
|
||||
|
||||
var payload = enu.Current;
|
||||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Color:
|
||||
TouchColorStack(this.colorStack, payload);
|
||||
continue;
|
||||
case MacroCode.EdgeColor:
|
||||
TouchColorStack(this.edgeColorStack, payload);
|
||||
continue;
|
||||
case MacroCode.ShadowColor:
|
||||
TouchColorStack(this.shadowColorStack, payload);
|
||||
continue;
|
||||
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
this.bold = u != 0;
|
||||
continue;
|
||||
case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
this.italic = u != 0;
|
||||
continue;
|
||||
case MacroCode.Edge when payload.TryGetExpression(out var e1, out var e2) &&
|
||||
e1.TryGetInt(out var v1) && e2.TryGetInt(out var v2):
|
||||
this.edge = new(v1, v2);
|
||||
continue;
|
||||
case MacroCode.Shadow when payload.TryGetExpression(out var e1, out var e2) &&
|
||||
e1.TryGetInt(out var v1) && e2.TryGetInt(out var v2):
|
||||
this.shadow = new(v1, v2);
|
||||
continue;
|
||||
case MacroCode.ColorType:
|
||||
TouchColorTypeStack(this.colorStack, this.colorTypes, payload);
|
||||
continue;
|
||||
case MacroCode.EdgeColorType:
|
||||
TouchColorTypeStack(this.edgeColorStack, this.edgeColorTypes, payload);
|
||||
continue;
|
||||
case MacroCode.Icon:
|
||||
case MacroCode.Icon2:
|
||||
{
|
||||
if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is not (var icon and not None) ||
|
||||
!this.gfd.TryGetEntry((uint)icon, out var gfdEntry) ||
|
||||
gfdEntry.IsEmpty)
|
||||
continue;
|
||||
|
||||
var useHq = state.FontSize > 19;
|
||||
var sizeScale = (state.FontSize + 1) / gfdEntry.Height;
|
||||
state.SetCurrentChannel(ChannelFore);
|
||||
state.Draw(
|
||||
offset + new Vector2(x, 0),
|
||||
gfdTextureSrv,
|
||||
Vector2.Zero,
|
||||
gfdEntry.Size * sizeScale,
|
||||
Vector2.Zero,
|
||||
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
|
||||
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1);
|
||||
width = Math.Max(width, x + (gfdEntry.Width * sizeScale));
|
||||
x += MathF.Round(gfdEntry.Width * sizeScale);
|
||||
lastRuneRepr = '\0';
|
||||
continue;
|
||||
}
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ToPrintableRune(c.EffectiveChar) is not { } rune)
|
||||
continue;
|
||||
|
||||
var runeRepr = rune.Value is >= 0 and < char.MaxValue ? (char)rune.Value : '\uFFFE';
|
||||
if (runeRepr != 0)
|
||||
{
|
||||
var dist = state.Font.GetDistanceAdjustmentForPair(lastRuneRepr, runeRepr);
|
||||
ref var g = ref *(ImGuiHelpers.ImFontGlyphReal*)state.Font.FindGlyph(runeRepr).NativePtr;
|
||||
|
||||
var dyItalic = this.italic
|
||||
? new Vector2(state.Font.FontSize - g.Y0, state.Font.FontSize - g.Y1) / 6
|
||||
: Vector2.Zero;
|
||||
|
||||
if (this.shadow != Vector2.Zero && this.shadowColorStack[^1] >= 0x1000000)
|
||||
{
|
||||
state.SetCurrentChannel(ChannelShadow);
|
||||
state.Draw(
|
||||
offset + this.shadow + new Vector2(x + dist, 0),
|
||||
g,
|
||||
dyItalic,
|
||||
this.shadowColorStack[^1]);
|
||||
}
|
||||
|
||||
if (this.edge != Vector2.Zero && this.edgeColorStack[^1] >= 0x1000000)
|
||||
{
|
||||
state.SetCurrentChannel(ChannelEdge);
|
||||
for (var dx = -this.edge.X; dx <= this.edge.X; dx++)
|
||||
{
|
||||
for (var dy = -this.edge.Y; dy <= this.edge.Y; dy++)
|
||||
{
|
||||
if (dx == 0 && dy == 0)
|
||||
continue;
|
||||
|
||||
state.Draw(offset + new Vector2(x + dist + dx, dy), g, dyItalic, this.edgeColorStack[^1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.SetCurrentChannel(ChannelFore);
|
||||
for (var dx = this.bold ? 1 : 0; dx >= 0; dx--)
|
||||
state.Draw(offset + new Vector2(x + dist + dx, 0), g, dyItalic, this.colorStack[^1]);
|
||||
|
||||
width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale));
|
||||
x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
|
||||
}
|
||||
|
||||
lastRuneRepr = runeRepr;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static void TouchColorStack(List<uint> stack, ReadOnlySePayloadSpan payload)
|
||||
{
|
||||
if (!payload.TryGetExpression(out var expr))
|
||||
return;
|
||||
if (expr.TryGetPlaceholderExpression(out var p) && p == (int)ExpressionType.StackColor && stack.Count > 1)
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
else if (expr.TryGetUInt(out var u))
|
||||
stack.Add(u);
|
||||
}
|
||||
|
||||
static void TouchColorTypeStack(List<uint> stack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
|
||||
{
|
||||
if (!payload.TryGetExpression(out var expr))
|
||||
return;
|
||||
if (!expr.TryGetUInt(out var u))
|
||||
return;
|
||||
if (u != 0)
|
||||
stack.Add(u < colorTypes.Length ? colorTypes[u] : 0u);
|
||||
else if (stack.Count > 1)
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
|
||||
{
|
||||
var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
|
||||
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
|
||||
return None;
|
||||
|
||||
var payload = e.Current;
|
||||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Icon
|
||||
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
|
||||
return (BitmapFontIcon)iconId;
|
||||
case MacroCode.Icon2
|
||||
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
|
||||
var configName = (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerShoulderLeft => SystemConfigOption.PadButton_L1,
|
||||
ControllerShoulderRight => SystemConfigOption.PadButton_R1,
|
||||
ControllerTriggerLeft => SystemConfigOption.PadButton_L2,
|
||||
ControllerTriggerRight => SystemConfigOption.PadButton_R2,
|
||||
ControllerButton3 => SystemConfigOption.PadButton_Triangle,
|
||||
ControllerButton1 => SystemConfigOption.PadButton_Cross,
|
||||
ControllerButton0 => SystemConfigOption.PadButton_Circle,
|
||||
ControllerButton2 => SystemConfigOption.PadButton_Square,
|
||||
ControllerStart => SystemConfigOption.PadButton_Start,
|
||||
ControllerBack => SystemConfigOption.PadButton_Select,
|
||||
ControllerAnalogLeftStick => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickIn => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickUpDown => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickLeftRight => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogRightStick => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickIn => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickUpDown => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickLeftRight => SystemConfigOption.PadButton_RS,
|
||||
_ => (SystemConfigOption?)null,
|
||||
};
|
||||
|
||||
if (configName is null || !this.gameConfig.TryGet(configName.Value, out PadButtonValue pb))
|
||||
return (BitmapFontIcon)iconId;
|
||||
|
||||
return pb switch
|
||||
{
|
||||
PadButtonValue.Autorun_Support => ControllerShoulderLeft,
|
||||
PadButtonValue.Hotbar_Set_Change => ControllerShoulderRight,
|
||||
PadButtonValue.XHB_Left_Start => ControllerTriggerLeft,
|
||||
PadButtonValue.XHB_Right_Start => ControllerTriggerRight,
|
||||
PadButtonValue.Jump => ControllerButton3,
|
||||
PadButtonValue.Accept => ControllerButton1,
|
||||
PadButtonValue.Cancel => ControllerButton0,
|
||||
PadButtonValue.Map_Sub => ControllerButton2,
|
||||
PadButtonValue.MainCommand => ControllerStart,
|
||||
PadButtonValue.HUD_Select => ControllerBack,
|
||||
PadButtonValue.Move_Operation => (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerAnalogLeftStick => ControllerAnalogLeftStick,
|
||||
ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn,
|
||||
ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown,
|
||||
ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight,
|
||||
ControllerAnalogRightStick => ControllerAnalogLeftStick,
|
||||
ControllerAnalogRightStickIn => ControllerAnalogLeftStickIn,
|
||||
ControllerAnalogRightStickUpDown => ControllerAnalogLeftStickUpDown,
|
||||
ControllerAnalogRightStickLeftRight => ControllerAnalogLeftStickLeftRight,
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
},
|
||||
PadButtonValue.Camera_Operation => (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerAnalogLeftStick => ControllerAnalogRightStick,
|
||||
ControllerAnalogLeftStickIn => ControllerAnalogRightStickIn,
|
||||
ControllerAnalogLeftStickUpDown => ControllerAnalogRightStickUpDown,
|
||||
ControllerAnalogLeftStickLeftRight => ControllerAnalogRightStickLeftRight,
|
||||
ControllerAnalogRightStick => ControllerAnalogRightStick,
|
||||
ControllerAnalogRightStickIn => ControllerAnalogRightStickIn,
|
||||
ControllerAnalogRightStickUpDown => ControllerAnalogRightStickUpDown,
|
||||
ControllerAnalogRightStickLeftRight => ControllerAnalogRightStickLeftRight,
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
},
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
};
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private readonly record struct TextFragment(
|
||||
int From,
|
||||
int To,
|
||||
Vector2 Offset,
|
||||
float VisibleWidth,
|
||||
float AdvanceWidth,
|
||||
float AdvanceWidthWithoutLastRune,
|
||||
bool MandatoryBreakAfter,
|
||||
bool EndsWithSoftHyphen,
|
||||
char FirstRuneRepr,
|
||||
char LastRuneRepr,
|
||||
char LastRuneRepr2)
|
||||
{
|
||||
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.MandatoryBreakAfter;
|
||||
}
|
||||
|
||||
private ref struct DrawState
|
||||
{
|
||||
public readonly ReadOnlySeStringSpan Raw;
|
||||
public readonly float FontSize;
|
||||
public readonly float FontSizeScale;
|
||||
public readonly Vector2 ScreenOffset;
|
||||
|
||||
public ImDrawListPtr DrawList;
|
||||
public ImDrawListSplitterPtr Splitter;
|
||||
public ImFontPtr Font;
|
||||
|
||||
public DrawState(
|
||||
ReadOnlySeStringSpan raw,
|
||||
ImDrawListPtr drawList,
|
||||
ImDrawListSplitterPtr splitter,
|
||||
ImFontPtr font,
|
||||
float fontSize,
|
||||
Vector2 screenOffset)
|
||||
{
|
||||
this.Raw = raw;
|
||||
this.DrawList = drawList;
|
||||
this.Splitter = splitter;
|
||||
this.Font = font;
|
||||
this.FontSize = fontSize;
|
||||
this.FontSizeScale = fontSize / font.FontSize;
|
||||
this.ScreenOffset = screenOffset;
|
||||
|
||||
splitter.Split(drawList, ChannelCount);
|
||||
}
|
||||
|
||||
public void SetCurrentChannel(int channelIndex) => this.Splitter.SetCurrentChannel(this.DrawList, channelIndex);
|
||||
|
||||
public void Draw(Vector2 offset, in ImGuiHelpers.ImFontGlyphReal g, Vector2 dyItalic, uint color) =>
|
||||
this.Draw(
|
||||
offset,
|
||||
this.Font.ContainerAtlas.Textures[g.TextureIndex].TexID,
|
||||
g.XY0 * this.FontSizeScale,
|
||||
g.XY1 * this.FontSizeScale,
|
||||
dyItalic * this.FontSizeScale,
|
||||
g.UV0,
|
||||
g.UV1,
|
||||
color);
|
||||
|
||||
public void Draw(
|
||||
Vector2 offset,
|
||||
nint igTextureId,
|
||||
Vector2 xy0,
|
||||
Vector2 xy1,
|
||||
Vector2 dyItalic,
|
||||
Vector2 uv0,
|
||||
Vector2 uv1,
|
||||
uint color = uint.MaxValue)
|
||||
{
|
||||
offset += this.ScreenOffset;
|
||||
this.DrawList.AddImageQuad(
|
||||
igTextureId,
|
||||
offset + new Vector2(xy0.X + dyItalic.X, xy0.Y),
|
||||
offset + new Vector2(xy0.X + dyItalic.Y, xy1.Y),
|
||||
offset + new Vector2(xy1.X + dyItalic.Y, xy1.Y),
|
||||
offset + new Vector2(xy1.X + dyItalic.X, xy0.Y),
|
||||
new(uv0.X, uv0.Y),
|
||||
new(uv0.X, uv1.Y),
|
||||
new(uv1.X, uv1.Y),
|
||||
new(uv1.X, uv0.Y),
|
||||
color);
|
||||
}
|
||||
|
||||
public TextFragment CreateFragment(
|
||||
SeStringRenderer renderer,
|
||||
int from,
|
||||
int to,
|
||||
bool mandatoryBreakAfter,
|
||||
Vector2 offset,
|
||||
float wrapWidth = float.PositiveInfinity)
|
||||
{
|
||||
var lastNonSpace = from;
|
||||
|
||||
var x = 0f;
|
||||
var w = 0f;
|
||||
var visibleWidth = 0f;
|
||||
var advanceWidth = 0f;
|
||||
var prevAdvanceWidth = 0f;
|
||||
var firstRuneRepr = char.MaxValue;
|
||||
var lastRuneRepr = default(char);
|
||||
var lastRuneRepr2 = default(char);
|
||||
var endsWithSoftHyphen = false;
|
||||
foreach (var c in UtfEnumerator.From(this.Raw.Data[from..to], UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
prevAdvanceWidth = x;
|
||||
lastRuneRepr2 = lastRuneRepr;
|
||||
endsWithSoftHyphen = c.EffectiveChar == SoftHyphen;
|
||||
|
||||
var byteOffset = from + c.ByteOffset;
|
||||
var isBreakableWhitespace = false;
|
||||
if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } &&
|
||||
renderer.GetBitmapFontIconFor(this.Raw.Data[byteOffset..]) is var icon and not None &&
|
||||
renderer.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
|
||||
!gfdEntry.IsEmpty)
|
||||
{
|
||||
var sizeScale = (this.FontSize + 1) / gfdEntry.Height;
|
||||
w = Math.Max(w, x + (gfdEntry.Width * sizeScale));
|
||||
x += MathF.Round(gfdEntry.Width * sizeScale);
|
||||
lastRuneRepr = default;
|
||||
}
|
||||
else if (ToPrintableRune(c.EffectiveChar) is { } rune)
|
||||
{
|
||||
var runeRepr = rune.Value is >= 0 and < char.MaxValue ? (char)rune.Value : '\uFFFE';
|
||||
if (runeRepr != 0)
|
||||
{
|
||||
var dist = this.Font.GetDistanceAdjustmentForPair(lastRuneRepr, runeRepr);
|
||||
ref var g = ref *(ImGuiHelpers.ImFontGlyphReal*)this.Font.FindGlyph(runeRepr).NativePtr;
|
||||
w = Math.Max(w, x + ((dist + g.X1) * this.FontSizeScale));
|
||||
x += MathF.Round((dist + g.AdvanceX) * this.FontSizeScale);
|
||||
}
|
||||
|
||||
isBreakableWhitespace = Rune.IsWhiteSpace(rune) &&
|
||||
UnicodeData.LineBreak[rune.Value] is not UnicodeLineBreakClass.GL;
|
||||
lastRuneRepr = runeRepr;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstRuneRepr == char.MaxValue)
|
||||
firstRuneRepr = lastRuneRepr;
|
||||
|
||||
if (isBreakableWhitespace)
|
||||
{
|
||||
advanceWidth = x;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (w > wrapWidth && lastNonSpace != from && !endsWithSoftHyphen)
|
||||
{
|
||||
to = byteOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
advanceWidth = x;
|
||||
visibleWidth = w;
|
||||
lastNonSpace = byteOffset + c.ByteLength;
|
||||
}
|
||||
}
|
||||
|
||||
return new(
|
||||
from,
|
||||
to,
|
||||
offset,
|
||||
visibleWidth,
|
||||
advanceWidth,
|
||||
prevAdvanceWidth,
|
||||
mandatoryBreakAfter,
|
||||
endsWithSoftHyphen,
|
||||
firstRuneRepr,
|
||||
lastRuneRepr,
|
||||
lastRuneRepr2);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,366 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeEastAsianWidthClass;
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeGeneralCategory;
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeLineBreakClass;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
/// <summary>Enumerates line break offsets.</summary>
|
||||
internal ref struct LineBreakEnumerator
|
||||
{
|
||||
private readonly UtfEnumeratorFlags enumeratorFlags;
|
||||
private readonly int dataLength;
|
||||
|
||||
private UtfEnumerator enumerator;
|
||||
private Entry class1;
|
||||
private Entry class2;
|
||||
|
||||
private Entry space1;
|
||||
private Entry space2;
|
||||
private bool spaceStreak;
|
||||
|
||||
private int consecutiveRegionalIndicators;
|
||||
|
||||
private bool finished;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary>
|
||||
/// <param name="data">UTF-N byte sequence.</param>
|
||||
/// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param>
|
||||
public LineBreakEnumerator(
|
||||
ReadOnlySpan<byte> data,
|
||||
UtfEnumeratorFlags enumeratorFlags = UtfEnumeratorFlags.Default)
|
||||
{
|
||||
this.enumerator = UtfEnumerator.From(data, enumeratorFlags);
|
||||
this.enumeratorFlags = enumeratorFlags;
|
||||
this.dataLength = data.Length;
|
||||
}
|
||||
|
||||
private LineBreakEnumerator(
|
||||
int dataLength,
|
||||
UtfEnumerator enumerator,
|
||||
UtfEnumeratorFlags enumeratorFlags)
|
||||
{
|
||||
this.dataLength = dataLength;
|
||||
this.enumerator = enumerator;
|
||||
this.enumeratorFlags = enumeratorFlags;
|
||||
}
|
||||
|
||||
private enum LineBreakMode : byte
|
||||
{
|
||||
Prohibited,
|
||||
Mandatory,
|
||||
Optional,
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerator{T}.Current"/>
|
||||
public (int ByteOffset, bool Mandatory) Current { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="IEnumerator.MoveNext"/>
|
||||
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.finished)
|
||||
return false;
|
||||
|
||||
while (this.enumerator.MoveNext())
|
||||
{
|
||||
var effectiveInt =
|
||||
this.enumerator.Current.IsSeStringPayload
|
||||
? UtfEnumerator.RepresentativeCharFor(this.enumerator.Current.MacroCode)
|
||||
: this.enumerator.Current.EffectiveInt;
|
||||
if (effectiveInt == -1)
|
||||
continue;
|
||||
|
||||
switch (this.HandleCharacter(effectiveInt))
|
||||
{
|
||||
case LineBreakMode.Mandatory:
|
||||
this.Current = (this.enumerator.Current.ByteOffset, true);
|
||||
return true;
|
||||
case LineBreakMode.Optional:
|
||||
this.Current = (this.enumerator.Current.ByteOffset, false);
|
||||
return true;
|
||||
case LineBreakMode.Prohibited:
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Start and end of text:
|
||||
// LB3 Always break at the end of text.
|
||||
this.Current = (this.dataLength, true);
|
||||
this.finished = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
public LineBreakEnumerator GetEnumerator() =>
|
||||
new(this.dataLength, this.enumerator.GetEnumerator(), this.enumeratorFlags);
|
||||
|
||||
private LineBreakMode HandleCharacter(int c)
|
||||
{
|
||||
// https://unicode.org/reports/tr14/#Algorithm
|
||||
|
||||
// 6.1 Non-tailorable Line Breaking Rules
|
||||
// Resolve line breaking classes:
|
||||
// LB1 Assign a line breaking class to each code point of the input.
|
||||
// => done inside Entry ctor
|
||||
var curr = new Entry(c);
|
||||
var (prev1, prev2) = (this.class1, this.class2);
|
||||
(this.class2, this.class1) = (this.class1, curr);
|
||||
|
||||
if (curr.Class == RI)
|
||||
this.consecutiveRegionalIndicators++;
|
||||
else
|
||||
this.consecutiveRegionalIndicators = 0;
|
||||
|
||||
var (prevSpaceStreak, prevSpace1, prevSpace2) = (this.spaceStreak, this.space1, this.space2);
|
||||
this.spaceStreak = curr.Class == SP;
|
||||
if (this.spaceStreak && !prevSpaceStreak)
|
||||
(this.space1, this.space2) = (prev1, prev2);
|
||||
|
||||
if (!prevSpaceStreak)
|
||||
(prevSpace1, prevSpace2) = (prev1, prev2);
|
||||
|
||||
// Start and end of text:
|
||||
// LB2 Never break at the start of text.
|
||||
if (prev1.Class is sot)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Mandatory breaks:
|
||||
// LB4 Always break after hard line breaks.
|
||||
if (prev1.Class is BK)
|
||||
return LineBreakMode.Mandatory;
|
||||
|
||||
// LB5 Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks.
|
||||
if (prev2.Class is CR && prev1.Class is LF)
|
||||
return LineBreakMode.Mandatory;
|
||||
if (prev1.Class is CR && curr.Class is LF)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is CR or LF or NL)
|
||||
return LineBreakMode.Mandatory;
|
||||
|
||||
// LB6 Do not break before hard line breaks.
|
||||
if (curr.Class is BK or CR or LF or NL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Explicit breaks and non-breaks:
|
||||
// LB7 Do not break before spaces or zero width space.
|
||||
if (curr.Class is SP or ZW)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB8 Break before any character following a zero-width space, even if one or more spaces intervene.
|
||||
if (prev1.Class is ZW)
|
||||
return LineBreakMode.Optional;
|
||||
if (prevSpaceStreak && prevSpace1.Class is ZW)
|
||||
return LineBreakMode.Optional;
|
||||
|
||||
// LB8a Do not break after a zero width joiner.
|
||||
if (prev1.Class is ZWJ)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Combining marks:
|
||||
// LB9 Do not break a combining character sequence; treat it as if it has the line breaking class of the base character in all of the following rules. Treat ZWJ as if it were CM.
|
||||
// ?
|
||||
|
||||
// LB10 Treat any remaining combining mark or ZWJ as AL.
|
||||
if (curr.Class is CM or ZWJ)
|
||||
this.class1 = curr = new('A');
|
||||
|
||||
// Word joiner:
|
||||
// LB11 Do not break before or after Word joiner and related characters.
|
||||
if (prev1.Class is WJ || curr.Class is WJ)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Non-breaking characters:
|
||||
// LB12 Do not break after NBSP and related characters.
|
||||
if (prev1.Class is GL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// 6.2 Tailorable Line Breaking Rules
|
||||
// Non-breaking characters:
|
||||
// LB12a Do not break before NBSP and related characters, except after spaces and hyphens.
|
||||
if (prev1.Class is not SP and not BA and not HY &&
|
||||
curr.Class is GL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Opening and closing:
|
||||
// LB13 Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces.
|
||||
if (curr.Class is CL or CP or EX or IS or SY)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB14 Do not break after ‘[’, even after spaces.
|
||||
if (prevSpace1.Class is OP)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB15a Do not break after an unresolved initial punctuation that lies at the start of the line, after a space, after opening punctuation, or after an unresolved quotation mark, even after spaces.
|
||||
if (prevSpace2.Class is sot or BK or CR or LF or NL or OP or QU or GL or SP or ZW &&
|
||||
prevSpace1.Class is QU &&
|
||||
prevSpace1.GeneralCategory is Pi)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
var next = this.enumerator.TryPeekNext(out var nextSubsequence, out _)
|
||||
? new Entry(nextSubsequence.EffectiveChar)
|
||||
: new(eot);
|
||||
|
||||
// LB15b Do not break before an unresolved final punctuation that lies at the end of the line, before a space, before a prohibited break, or before an unresolved quotation mark, even after spaces.
|
||||
if (curr.Class is QU && curr.GeneralCategory is Pf &&
|
||||
next.Class is SP or GL or WJ or CL or QU or CP or EX or IS or SY or BK or CR or LF or NL or ZW or eot)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB16 Do not break between closing punctuation and a nonstarter (lb=NS), even with intervening spaces.
|
||||
if (prevSpace1.Class is CL or CP && next.Class is NS)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB17 Do not break within ‘——’, even with intervening spaces.
|
||||
if (prevSpace1.Class is B2 && next.Class is B2)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Spaces:
|
||||
// LB18 Break after spaces.
|
||||
if (prev1.Class is SP)
|
||||
return LineBreakMode.Optional;
|
||||
|
||||
// Special case rules:
|
||||
// LB19 Do not break before or after quotation marks, such as ‘ ” ’.
|
||||
if (prev1.Class is QU || curr.Class is QU)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB20 Break before and after unresolved CB.
|
||||
if (prev1.Class is CB || curr.Class is CB)
|
||||
return LineBreakMode.Optional;
|
||||
|
||||
// LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents.
|
||||
if (curr.Class is BA or HY or NS || prev1.Class is BB)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB21a Don't break after Hebrew + Hyphen.
|
||||
if (prev2.Class is HL && prev1.Class is HY or BA)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB21b Don’t break between Solidus and Hebrew letters.
|
||||
if (prev1.Class is SY && curr.Class is HL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB22 Do not break before ellipses.
|
||||
if (curr.Class is IN)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Numbers:
|
||||
// LB23 Do not break between digits and letters.
|
||||
if (prev1.Class is AL or HL && curr.Class is NU)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is NU && curr.Class is AL or HL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB23a Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes.
|
||||
if (prev1.Class is PR && curr.Class is ID or EB or EM)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is ID or EB or EM && curr.Class is PR)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB24 Do not break between numeric prefix/postfix and letters, or between letters and prefix/postfix.
|
||||
if (prev1.Class is PR or PO && curr.Class is AL or HL)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is AL or HL && curr.Class is PR or PO)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB25 Do not break between the following pairs of classes relevant to numbers:
|
||||
if ((prev1.Class, curr.Class) is (CL, PO) or (CP, PO) or (CL, PR) or (CP, PR) or (NU, PO) or (NU, PR)
|
||||
or (PO, OP) or (PO, NU) or (PR, OP) or (PR, NU) or (HY, NU) or (IS, NU) or (NU, NU) or (SY, NU))
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Korean syllable blocks:
|
||||
// LB26 Do not break a Korean syllable.
|
||||
if (prev1.Class is JL && curr.Class is JL or JV or H2 or H3)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is JV or H2 && curr.Class is JV or JT)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB27 Treat a Korean Syllable Block the same as ID.
|
||||
if (prev1.Class is JL or JV or JT or H2 or H3 && curr.Class is PO)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is PR && curr.Class is JL or JV or JT or H2 or H3)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// Finally, join alphabetic letters into words and break everything else.
|
||||
// LB28 Do not break between alphabetics (“at”).
|
||||
if (prev1.Class is AL or HL && curr.Class is AL or HL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB28a Do not break inside the orthographic syllables of Brahmic scripts.
|
||||
// TODO: what's "◌"?
|
||||
if (prev1.Class is AP && curr.Class is AK or AS)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is AK or AS && curr.Class is VF or VI)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev2.Class is AK or AS && prev1.Class is VI && curr.Class is AK)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is AK or AS && curr.Class is AK or AS && next.Class is VF)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB29 Do not break between numeric punctuation and alphabetics (“e.g.”).
|
||||
if (prev1.Class is IS && curr.Class is AL or HL)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB30 Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses.
|
||||
if (prev1.Class is AL or HL or NU &&
|
||||
curr.Class is OP && curr.EastAsianWidth is not F and not W and not H)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.Class is CP && prev1.EastAsianWidth is not F and not W and not H &&
|
||||
curr.Class is AL or HL or NU)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB30a Break between two regional indicator symbols if and only if there are an even number of regional indicators preceding the position of the break.
|
||||
if (this.consecutiveRegionalIndicators % 2 == 0)
|
||||
return LineBreakMode.Optional;
|
||||
|
||||
// LB30b Do not break between an emoji base (or potential emoji) and an emoji modifier.
|
||||
if (prev1.Class is EB && curr.Class is EM)
|
||||
return LineBreakMode.Prohibited;
|
||||
if (prev1.GeneralCategory is Cn &&
|
||||
(prev1.EmojiProperty & UnicodeEmojiProperty.Extended_Pictographic) != 0 &&
|
||||
curr.Class is EM)
|
||||
return LineBreakMode.Prohibited;
|
||||
|
||||
// LB31 Break everywhere else.
|
||||
return LineBreakMode.Optional;
|
||||
}
|
||||
|
||||
private readonly struct Entry
|
||||
{
|
||||
public readonly UnicodeLineBreakClass Class;
|
||||
public readonly UnicodeGeneralCategory GeneralCategory;
|
||||
public readonly UnicodeEastAsianWidthClass EastAsianWidth;
|
||||
public readonly UnicodeEmojiProperty EmojiProperty;
|
||||
|
||||
public Entry(int c)
|
||||
{
|
||||
this.Class = UnicodeData.LineBreak[c] switch
|
||||
{
|
||||
AI or SG or XX => AL,
|
||||
SA when UnicodeData.GeneralCategory[c] is Mn or Mc => CM,
|
||||
SA => AL,
|
||||
CJ => NS,
|
||||
var x => x,
|
||||
};
|
||||
this.GeneralCategory = UnicodeData.GeneralCategory[c];
|
||||
this.EastAsianWidth = UnicodeData.EastAsianWidth[c];
|
||||
this.EmojiProperty = UnicodeData.EmojiProperty[c];
|
||||
}
|
||||
|
||||
public Entry(
|
||||
UnicodeLineBreakClass lineBreakClass,
|
||||
UnicodeGeneralCategory generalCategory = Cn,
|
||||
UnicodeEastAsianWidthClass eastAsianWidth = N,
|
||||
UnicodeEmojiProperty emojiProperty = 0)
|
||||
{
|
||||
this.Class = lineBreakClass;
|
||||
this.GeneralCategory = generalCategory;
|
||||
this.EastAsianWidth = eastAsianWidth;
|
||||
this.EmojiProperty = emojiProperty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
/// <summary>Stores unicode data.</summary>
|
||||
internal static class UnicodeData
|
||||
{
|
||||
/// <summary>Line break classes.</summary>
|
||||
public static readonly UnicodeLineBreakClass[] LineBreak;
|
||||
|
||||
/// <summary>East asian width classes.</summary>
|
||||
public static readonly UnicodeEastAsianWidthClass[] EastAsianWidth;
|
||||
|
||||
/// <summary>General categories.</summary>
|
||||
public static readonly UnicodeGeneralCategory[] GeneralCategory;
|
||||
|
||||
/// <summary>Emoji properties.</summary>
|
||||
public static readonly UnicodeEmojiProperty[] EmojiProperty;
|
||||
|
||||
static UnicodeData()
|
||||
{
|
||||
// File is from https://www.unicode.org/Public/UCD/latest/ucd/LineBreak.txt
|
||||
LineBreak =
|
||||
Parse(
|
||||
typeof(UnicodeData).Assembly.GetManifestResourceStream("LineBreak.txt")!,
|
||||
UnicodeLineBreakClass.XX);
|
||||
|
||||
// https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
|
||||
EastAsianWidth =
|
||||
Parse(
|
||||
typeof(UnicodeData).Assembly.GetManifestResourceStream("EastAsianWidth.txt")!,
|
||||
UnicodeEastAsianWidthClass.N);
|
||||
|
||||
// https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedGeneralCategory.txt
|
||||
GeneralCategory =
|
||||
Parse(
|
||||
typeof(UnicodeData).Assembly.GetManifestResourceStream("DerivedGeneralCategory.txt")!,
|
||||
UnicodeGeneralCategory.Cn);
|
||||
|
||||
// https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt
|
||||
EmojiProperty =
|
||||
Parse(
|
||||
typeof(UnicodeData).Assembly.GetManifestResourceStream("emoji-data.txt")!,
|
||||
UnicodeEmojiProperty.Emoji);
|
||||
}
|
||||
|
||||
private static T[] Parse<T>(Stream stream, T defaultValue)
|
||||
where T : unmanaged, Enum
|
||||
{
|
||||
if (Unsafe.SizeOf<T>() != 1)
|
||||
throw new InvalidOperationException("Enum must be of size 1 byte");
|
||||
|
||||
var isFlag = typeof(T).GetCustomAttribute<FlagsAttribute>() is not null;
|
||||
using var sr = new StreamReader(stream);
|
||||
var res = new T[0x110000];
|
||||
res.AsSpan().Fill(defaultValue);
|
||||
for (string? line; (line = sr.ReadLine()) != null;)
|
||||
{
|
||||
var span = line.AsSpan();
|
||||
|
||||
// strip comment
|
||||
var i = span.IndexOf('#');
|
||||
if (i != -1)
|
||||
span = span[..i];
|
||||
|
||||
span = span.Trim();
|
||||
if (span.IsEmpty)
|
||||
continue;
|
||||
|
||||
// find delimiter
|
||||
i = span.IndexOf(';');
|
||||
if (i == -1)
|
||||
throw new InvalidDataException();
|
||||
|
||||
var range = span[..i].Trim();
|
||||
var entry = Enum.Parse<T>(span[(i + 1)..].Trim());
|
||||
|
||||
i = range.IndexOf("..");
|
||||
int from, to;
|
||||
if (i == -1)
|
||||
{
|
||||
from = int.Parse(range, NumberStyles.HexNumber);
|
||||
to = from + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
from = int.Parse(range[..i], NumberStyles.HexNumber);
|
||||
to = int.Parse(range[(i + 2)..], NumberStyles.HexNumber) + 1;
|
||||
}
|
||||
|
||||
if (from > char.MaxValue)
|
||||
continue;
|
||||
|
||||
from = Math.Min(from, char.MaxValue);
|
||||
to = Math.Min(to, char.MaxValue);
|
||||
if (isFlag)
|
||||
{
|
||||
foreach (ref var v in res.AsSpan()[from..to])
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (void* p = &v)
|
||||
*(byte*)p |= *(byte*)&entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
res.AsSpan()[from..to].Fill(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1600:Elements should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1602:Enumeration items should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
internal enum UnicodeEastAsianWidthClass : byte
|
||||
{
|
||||
A = 0,
|
||||
F = 1,
|
||||
H = 2,
|
||||
N = 3,
|
||||
Na = 4,
|
||||
W = 5,
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1600:Elements should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1602:Enumeration items should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
[Flags]
|
||||
internal enum UnicodeEmojiProperty : byte
|
||||
{
|
||||
Emoji = 1 << 0,
|
||||
Emoji_Presentation = 1 << 1,
|
||||
Emoji_Modifier_Base = 1 << 2,
|
||||
Emoji_Modifier = 1 << 3,
|
||||
Emoji_Component = 1 << 4,
|
||||
Extended_Pictographic = 1 << 5,
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1600:Elements should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1602:Enumeration items should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
internal enum UnicodeGeneralCategory : byte
|
||||
{
|
||||
Cn = 0,
|
||||
Lu = 1,
|
||||
Ll = 2,
|
||||
Lt = 3,
|
||||
Lm = 4,
|
||||
Lo = 5,
|
||||
Mn = 6,
|
||||
Me = 7,
|
||||
Mc = 8,
|
||||
Nd = 9,
|
||||
Nl = 10,
|
||||
No = 11,
|
||||
Zs = 12,
|
||||
Zl = 13,
|
||||
Zp = 14,
|
||||
Cc = 15,
|
||||
Cf = 16,
|
||||
Co = 17,
|
||||
Cs = 18,
|
||||
Pd = 19,
|
||||
Ps = 20,
|
||||
Pe = 21,
|
||||
Pc = 22,
|
||||
Po = 23,
|
||||
Sm = 24,
|
||||
Sc = 25,
|
||||
Sk = 26,
|
||||
So = 27,
|
||||
Pi = 28,
|
||||
Pf = 29,
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1300:Element should begin with an uppercase letter",
|
||||
Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1600:Elements should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.DocumentationRules",
|
||||
"SA1602:Enumeration items should be documented",
|
||||
Justification = "Unicode Data")]
|
||||
internal enum UnicodeLineBreakClass : byte
|
||||
{
|
||||
/// <summary>Start of text.</summary>
|
||||
sot,
|
||||
|
||||
/// <summary>End of text.</summary>
|
||||
eot,
|
||||
|
||||
/// <summary>Mandatory Break; NL, PARAGRAPH SEPARATOR; Cause a line break (after).</summary>
|
||||
BK,
|
||||
|
||||
/// <summary>Carriage Return; CR; Cause a line break (after), except between CR and LF.</summary>
|
||||
CR,
|
||||
|
||||
/// <summary>Line Feed; LF; Cause a line break (after).</summary>
|
||||
LF,
|
||||
|
||||
/// <summary>Combining Mark; Combining marks, control codes; Prohibit a line break between the character and the preceding character.</summary>
|
||||
CM,
|
||||
|
||||
/// <summary>Next Line; NEL; Cause a line break (after).</summary>
|
||||
NL,
|
||||
|
||||
/// <summary>Surrogate; Surrogates; Do not occur in well-formed text.</summary>
|
||||
SG,
|
||||
|
||||
/// <summary>Word Joiner; WJ; Prohibit line breaks before and after.</summary>
|
||||
WJ,
|
||||
|
||||
/// <summary>Zero Width Space; ZWSP; Provide a break opportunity.</summary>
|
||||
ZW,
|
||||
|
||||
/// <summary>Non-breaking (“Glue”); CGJ, NBSP, ZWNBSP; Prohibit line breaks before and after.</summary>
|
||||
GL,
|
||||
|
||||
/// <summary>Space; SPACE; Enable indirect line breaks.</summary>
|
||||
SP,
|
||||
|
||||
/// <summary>Zero Width Joiner; Zero Width Joiner; Prohibit line breaks within joiner sequences.</summary>
|
||||
ZWJ,
|
||||
|
||||
/// <summary>Break Opportunity Before and After; Em dash; Provide a line break opportunity before and after the character.</summary>
|
||||
B2,
|
||||
|
||||
/// <summary>Break After; Spaces, hyphens; Generally provide a line break opportunity after the character.</summary>
|
||||
BA,
|
||||
|
||||
/// <summary>Break Before; Punctuation used in dictionaries; Generally provide a line break opportunity before the character.</summary>
|
||||
BB,
|
||||
|
||||
/// <summary>Hyphen; HYPHEN-MINUS; Provide a line break opportunity after the character, except in numeric context.</summary>
|
||||
HY,
|
||||
|
||||
/// <summary>Contingent Break Opportunity; Inline objects; Provide a line break opportunity contingent on additional information.</summary>
|
||||
CB,
|
||||
|
||||
/// <summary>Close Punctuation; “}”, “❳”, “⟫” etc.; Prohibit line breaks before.</summary>
|
||||
CL,
|
||||
|
||||
/// <summary>Close Parenthesis; “)”, “]”; Prohibit line breaks before.</summary>
|
||||
CP,
|
||||
|
||||
/// <summary>Exclamation/Interrogation; “!”, “?”, etc.; Prohibit line breaks before.</summary>
|
||||
EX,
|
||||
|
||||
/// <summary>Inseparable; Leaders; Allow only indirect line breaks between pairs.</summary>
|
||||
IN,
|
||||
|
||||
/// <summary>Nonstarter; “‼”, “‽”, “⁇”, “⁉”, etc.; Allow only indirect line breaks before.</summary>
|
||||
NS,
|
||||
|
||||
/// <summary>Open Punctuation; “(“, “[“, “{“, etc.; Prohibit line breaks after.</summary>
|
||||
OP,
|
||||
|
||||
/// <summary>Quotation; Quotation marks; Act like they are both opening and closing.</summary>
|
||||
QU,
|
||||
|
||||
/// <summary>Infix Numeric Separator; . ,; Prevent breaks after any and before numeric.</summary>
|
||||
IS,
|
||||
|
||||
/// <summary>Numeric; Digits; Form numeric expressions for line breaking purposes.</summary>
|
||||
NU,
|
||||
|
||||
/// <summary>Postfix Numeric; %, ¢; Do not break following a numeric expression.</summary>
|
||||
PO,
|
||||
|
||||
/// <summary>Prefix Numeric; $, £, ¥, etc.; Do not break in front of a numeric expression.</summary>
|
||||
PR,
|
||||
|
||||
/// <summary>Symbols Allowing Break After; /; Prevent a break before, and allow a break after.</summary>
|
||||
SY,
|
||||
|
||||
/// <summary>Ambiguous (Alphabetic or Ideographic); Characters with Ambiguous East Asian Width; Act like AL when the resolved EAW is N; otherwise, act as ID.</summary>
|
||||
AI,
|
||||
|
||||
/// <summary>Aksara; Consonants; Form orthographic syllables in Brahmic scripts.</summary>
|
||||
AK,
|
||||
|
||||
/// <summary>Alphabetic; Alphabets and regular symbols; Are alphabetic characters or symbols that are used with alphabetic characters.</summary>
|
||||
AL,
|
||||
|
||||
/// <summary>Aksara Pre-Base; Pre-base repha; Form orthographic syllables in Brahmic scripts.</summary>
|
||||
AP,
|
||||
|
||||
/// <summary>Aksara Start; Independent vowels; Form orthographic syllables in Brahmic scripts.</summary>
|
||||
AS,
|
||||
|
||||
/// <summary>Conditional Japanese Starter; Small kana; Treat as NS or ID for strict or normal breaking.</summary>
|
||||
CJ,
|
||||
|
||||
/// <summary>Emoji Base; All emoji allowing modifiers; Do not break from following Emoji Modifier.</summary>
|
||||
EB,
|
||||
|
||||
/// <summary>Emoji Modifier; Skin tone modifiers; Do not break from preceding Emoji Base.</summary>
|
||||
EM,
|
||||
|
||||
/// <summary>Hangul LV Syllable; Hangul; Form Korean syllable blocks.</summary>
|
||||
H2,
|
||||
|
||||
/// <summary>Hangul LVT Syllable; Hangul; Form Korean syllable blocks.</summary>
|
||||
H3,
|
||||
|
||||
/// <summary>Hebrew Letter; Hebrew; Do not break around a following hyphen; otherwise act as Alphabetic.</summary>
|
||||
HL,
|
||||
|
||||
/// <summary>Ideographic; Ideographs; Break before or after, except in some numeric context.</summary>
|
||||
ID,
|
||||
|
||||
/// <summary>Hangul L Jamo; Conjoining jamo; Form Korean syllable blocks.</summary>
|
||||
JL,
|
||||
|
||||
/// <summary>Hangul V Jamo; Conjoining jamo; Form Korean syllable blocks.</summary>
|
||||
JV,
|
||||
|
||||
/// <summary>Hangul T Jamo; Conjoining jamo; Form Korean syllable blocks.</summary>
|
||||
JT,
|
||||
|
||||
/// <summary>Regional Indicator; REGIONAL INDICATOR SYMBOL LETTER A .. Z; Keep pairs together. For pairs, break before and after other classes.</summary>
|
||||
RI,
|
||||
|
||||
/// <summary>Complex Context Dependent (South East Asian); South East Asian: Thai, Lao, Khmer; Provide a line break opportunity contingent on additional, language-specific context analysis.</summary>
|
||||
SA,
|
||||
|
||||
/// <summary>Virama Final; Viramas for final consonants; Form orthographic syllables in Brahmic scripts.</summary>
|
||||
VF,
|
||||
|
||||
/// <summary>Virama; Conjoining viramas; Form orthographic syllables in Brahmic scripts.</summary>
|
||||
VI,
|
||||
|
||||
/// <summary>Unknown; Most unassigned, private-use; Have as yet unknown line breaking behavior or unassigned code positions.</summary>
|
||||
XX,
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Lumina.Text;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
/// <summary>Enumerates a UTF-N byte sequence by codepoint.</summary>
|
||||
[DebuggerDisplay("{Current}/{data.Length} ({flags}, BE={isBigEndian})")]
|
||||
internal ref struct UtfEnumerator
|
||||
{
|
||||
private readonly ReadOnlySpan<byte> data;
|
||||
private readonly UtfEnumeratorFlags flags;
|
||||
private readonly byte numBytesPerUnit;
|
||||
private bool isBigEndian;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="UtfEnumerator"/> struct.</summary>
|
||||
/// <param name="data">UTF-N byte sequence.</param>
|
||||
/// <param name="flags">Enumeration flags.</param>
|
||||
public UtfEnumerator(ReadOnlySpan<byte> data, UtfEnumeratorFlags flags)
|
||||
{
|
||||
this.data = data;
|
||||
this.flags = flags;
|
||||
this.numBytesPerUnit = (this.flags & UtfEnumeratorFlags.UtfMask) switch
|
||||
{
|
||||
UtfEnumeratorFlags.Utf8 or UtfEnumeratorFlags.Utf8SeString => 1,
|
||||
UtfEnumeratorFlags.Utf16 => 2,
|
||||
UtfEnumeratorFlags.Utf32 => 4,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(this.flags), this.flags, "Multiple UTF flag specified."),
|
||||
};
|
||||
this.isBigEndian = (flags & UtfEnumeratorFlags.EndiannessMask) switch
|
||||
{
|
||||
UtfEnumeratorFlags.NativeEndian => !BitConverter.IsLittleEndian,
|
||||
UtfEnumeratorFlags.LittleEndian => false,
|
||||
UtfEnumeratorFlags.BigEndian => true,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(flags), flags, "Multiple endianness flag specified."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerator.Current"/>
|
||||
public Subsequence Current { get; private set; } = default;
|
||||
|
||||
/// <summary>Creates a new instance of the <see cref="UtfEnumerator"/> struct.</summary>
|
||||
/// <param name="data">UTF-N byte sequence.</param>
|
||||
/// <param name="flags">Enumeration flags.</param>
|
||||
/// <returns>A new enumerator.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UtfEnumerator From(ReadOnlySpan<byte> data, UtfEnumeratorFlags flags) => new(data, flags);
|
||||
|
||||
/// <summary>Creates a new instance of the <see cref="UtfEnumerator"/> struct.</summary>
|
||||
/// <param name="data">UTF-N byte sequence.</param>
|
||||
/// <param name="flags">Enumeration flags.</param>
|
||||
/// <returns>A new enumerator.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UtfEnumerator From(ReadOnlySpan<char> data, UtfEnumeratorFlags flags) =>
|
||||
new(MemoryMarshal.Cast<char, byte>(data), flags);
|
||||
|
||||
/// <summary>Gets the representative <c>char</c> for a given SeString macro code.</summary>
|
||||
/// <param name="macroCode">The macro code.</param>
|
||||
/// <returns>Representative <c>char</c>, or <see cref="char.MaxValue"/> if none.</returns>
|
||||
public static char RepresentativeCharFor(MacroCode macroCode) => macroCode switch
|
||||
{
|
||||
MacroCode.NewLine => '\u0085',
|
||||
MacroCode.SoftHyphen => '\u00AD',
|
||||
MacroCode.NonBreakingSpace => '\u00A0',
|
||||
MacroCode.Hyphen => '-',
|
||||
MacroCode.Icon or MacroCode.Icon2 => '\uFFFC',
|
||||
_ => char.MaxValue,
|
||||
};
|
||||
|
||||
/// <summary>Attempts to peek the next item.</summary>
|
||||
/// <param name="nextSubsequence">Retrieved next item.</param>
|
||||
/// <param name="isStillBigEndian">Whether it still should be parsed in big endian.</param>
|
||||
/// <returns><c>true</c> if anything is retrieved.</returns>
|
||||
/// <exception cref="EncoderFallbackException">The sequence is not a fully valid Unicode sequence, and
|
||||
/// <see cref="UtfEnumeratorFlags.ThrowOnFirstError"/> is set.</exception>
|
||||
public readonly bool TryPeekNext(out Subsequence nextSubsequence, out bool isStillBigEndian)
|
||||
{
|
||||
var offset = this.Current.ByteOffset + this.Current.ByteLength;
|
||||
isStillBigEndian = this.isBigEndian;
|
||||
while (true)
|
||||
{
|
||||
var subspan = this.data[offset..];
|
||||
|
||||
if (subspan.IsEmpty)
|
||||
{
|
||||
nextSubsequence = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
UtfValue value;
|
||||
int length;
|
||||
var isBroken =
|
||||
this.numBytesPerUnit switch
|
||||
{
|
||||
1 => !UtfValue.TryDecode8(subspan, out value, out length),
|
||||
2 => !UtfValue.TryDecode16(subspan, isStillBigEndian, out value, out length),
|
||||
4 => !UtfValue.TryDecode32(subspan, isStillBigEndian, out value, out length),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
if (!isBroken && value.IntValue == 0xFFFE)
|
||||
{
|
||||
if ((this.flags & UtfEnumeratorFlags.DisrespectByteOrderMask) == 0)
|
||||
{
|
||||
isStillBigEndian = !isStillBigEndian;
|
||||
value = 0xFEFF;
|
||||
}
|
||||
|
||||
if ((this.flags & UtfEnumeratorFlags.YieldByteOrderMask) == 0)
|
||||
{
|
||||
offset += length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBroken || !Rune.IsValid(value))
|
||||
{
|
||||
switch (this.flags & UtfEnumeratorFlags.ErrorHandlingMask)
|
||||
{
|
||||
case UtfEnumeratorFlags.ReplaceErrors:
|
||||
break;
|
||||
|
||||
case UtfEnumeratorFlags.IgnoreErrors:
|
||||
offset = Math.Min(offset + this.numBytesPerUnit, this.data.Length);
|
||||
continue;
|
||||
|
||||
case UtfEnumeratorFlags.ThrowOnFirstError:
|
||||
if (isBroken)
|
||||
throw new EncoderFallbackException($"0x{subspan[0]:X02} is not a valid sequence.");
|
||||
throw new EncoderFallbackException(
|
||||
$"U+{value.UIntValue:X08} is not a valid unicode codepoint.");
|
||||
|
||||
case UtfEnumeratorFlags.TerminateOnFirstError:
|
||||
default:
|
||||
nextSubsequence = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBroken)
|
||||
value = subspan[0];
|
||||
|
||||
if (value == SeString.StartByte && (this.flags & UtfEnumeratorFlags.Utf8SeString) != 0)
|
||||
{
|
||||
var e = new ReadOnlySeStringSpan(subspan).GetEnumerator();
|
||||
e.MoveNext();
|
||||
switch (this.flags & UtfEnumeratorFlags.ErrorHandlingMask)
|
||||
{
|
||||
case var _ when e.Current.Type is ReadOnlySePayloadType.Macro:
|
||||
nextSubsequence = Subsequence.FromPayload(
|
||||
e.Current.MacroCode,
|
||||
offset,
|
||||
e.Current.EnvelopeByteLength);
|
||||
return true;
|
||||
|
||||
case UtfEnumeratorFlags.ReplaceErrors:
|
||||
value = '\uFFFE';
|
||||
length = e.Current.EnvelopeByteLength;
|
||||
isBroken = true;
|
||||
break;
|
||||
|
||||
case UtfEnumeratorFlags.IgnoreErrors:
|
||||
offset = Math.Min(offset + e.Current.EnvelopeByteLength, this.data.Length);
|
||||
continue;
|
||||
|
||||
case UtfEnumeratorFlags.ThrowOnFirstError:
|
||||
throw new EncoderFallbackException("Invalid SeString payload.");
|
||||
|
||||
case UtfEnumeratorFlags.TerminateOnFirstError:
|
||||
default:
|
||||
nextSubsequence = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
nextSubsequence = Subsequence.FromUnicode(value, offset, length, isBroken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerator.MoveNext"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (!this.TryPeekNext(out var next, out var isStillBigEndian))
|
||||
return false;
|
||||
|
||||
this.Current = next;
|
||||
this.isBigEndian = isStillBigEndian;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public UtfEnumerator GetEnumerator() => new(this.data, this.flags);
|
||||
|
||||
/// <summary>A part of a UTF-N sequence containing one codepoint.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 16)]
|
||||
[DebuggerDisplay("[{ByteOffset}, {ByteLength}] {Value}")]
|
||||
public readonly struct Subsequence : IEquatable<Subsequence>
|
||||
{
|
||||
/// <summary>The codepoint. Valid if <see cref="IsSeStringPayload"/> is <c>false</c>.</summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly UtfValue Value;
|
||||
|
||||
/// <summary>The macro code. Valid if <see cref="IsSeStringPayload"/> is <c>true</c>.</summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly MacroCode MacroCode;
|
||||
|
||||
/// <summary>The offset of this part of a UTF-8 sequence.</summary>
|
||||
[FieldOffset(4)]
|
||||
public readonly int ByteOffset;
|
||||
|
||||
/// <summary>The length of this part of a UTF-8 sequence.</summary>
|
||||
/// <remarks>This may not match <see cref="UtfValue.Length8"/>, if <see cref="BrokenSequence"/> is <c>true</c>.
|
||||
/// </remarks>
|
||||
[FieldOffset(8)]
|
||||
public readonly int ByteLength;
|
||||
|
||||
/// <summary>Whether this part of the UTF-8 sequence is broken.</summary>
|
||||
[FieldOffset(12)]
|
||||
public readonly bool BrokenSequence;
|
||||
|
||||
/// <summary>Whether this part of the SeString sequence is a payload.</summary>
|
||||
[FieldOffset(13)]
|
||||
public readonly bool IsSeStringPayload;
|
||||
|
||||
/// <summary>Storage at byte offset 0, for fast <see cref="Equals(Subsequence)"/> implementation.</summary>
|
||||
[FieldOffset(0)]
|
||||
private readonly ulong storage0;
|
||||
|
||||
/// <summary>Storage at byte offset 8, for fast <see cref="Equals(Subsequence)"/> implementation.</summary>
|
||||
[FieldOffset(8)]
|
||||
private readonly ulong storage1;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="Subsequence"/> struct.</summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="byteOffset">The byte offset of this part of a UTF-N sequence.</param>
|
||||
/// <param name="byteLength">The byte length of this part of a UTF-N sequence.</param>
|
||||
/// <param name="brokenSequence">Whether this part of the UTF-N sequence is broken.</param>
|
||||
/// <param name="isSeStringPayload">Whether this part of the SeString sequence is a payload.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Subsequence(uint value, int byteOffset, int byteLength, bool brokenSequence, bool isSeStringPayload)
|
||||
{
|
||||
this.Value = new(value);
|
||||
this.ByteOffset = byteOffset;
|
||||
this.ByteLength = byteLength;
|
||||
this.BrokenSequence = brokenSequence;
|
||||
this.IsSeStringPayload = isSeStringPayload;
|
||||
}
|
||||
|
||||
/// <summary>Gets the effective <c>char</c> value, with invalid or non-representable codepoints replaced.
|
||||
/// </summary>
|
||||
/// <value><see cref="char.MaxValue"/> if the character should not be displayed at all.</value>
|
||||
public char EffectiveChar => this.EffectiveInt is var i and >= 0 and < char.MaxValue ? (char)i : char.MaxValue;
|
||||
|
||||
/// <summary>Gets the effective <c>int</c> value, with invalid codepoints replaced.</summary>
|
||||
/// <value><see cref="char.MaxValue"/> if the character should not be displayed at all.</value>
|
||||
public int EffectiveInt =>
|
||||
this.IsSeStringPayload
|
||||
? RepresentativeCharFor(this.MacroCode)
|
||||
: this.BrokenSequence || !this.Value.TryGetRune(out var rune)
|
||||
? 0xFFFD
|
||||
: rune.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(Subsequence left, Subsequence right) => left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(Subsequence left, Subsequence right) => !left.Equals(right);
|
||||
|
||||
/// <summary>Creates a new instance of the <see cref="Subsequence"/> struct from a Unicode value.</summary>
|
||||
/// <param name="codepoint">The codepoint.</param>
|
||||
/// <param name="byteOffset">The byte offset of this part of a UTF-N sequence.</param>
|
||||
/// <param name="byteLength">The byte length of this part of a UTF-N sequence.</param>
|
||||
/// <param name="brokenSequence">Whether this part of the UTF-N sequence is broken.</param>
|
||||
/// <returns>A new instance.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Subsequence FromUnicode(uint codepoint, int byteOffset, int byteLength, bool brokenSequence) =>
|
||||
new(codepoint, byteOffset, byteLength, brokenSequence, false);
|
||||
|
||||
/// <summary>Creates a new instance of the <see cref="Subsequence"/> struct from a SeString payload.</summary>
|
||||
/// <param name="macroCode">The macro code.</param>
|
||||
/// <param name="byteOffset">The byte offset of this part of a UTF-N sequence.</param>
|
||||
/// <param name="byteLength">The byte length of this part of a UTF-N sequence.</param>
|
||||
/// <returns>A new instance.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Subsequence FromPayload(MacroCode macroCode, int byteOffset, int byteLength) =>
|
||||
new((uint)macroCode, byteOffset, byteLength, false, true);
|
||||
|
||||
/// <summary>Tests whether this subsequence contains a valid Unicode codepoint.</summary>
|
||||
/// <returns><c>true</c> if this subsequence contains a valid Unicode codepoint.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsValid() => !this.BrokenSequence && Rune.IsValid(this.Value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(Subsequence other) => this.storage0 == other.storage0 && this.storage1 == other.storage1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override bool Equals(object? obj) => obj is Subsequence other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode() => HashCode.Combine(this.storage0, this.storage1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
/// <summary>Flags on enumerating a unicode sequence.</summary>
|
||||
[Flags]
|
||||
internal enum UtfEnumeratorFlags
|
||||
{
|
||||
/// <summary>Use the default configuration of <see cref="Utf8"/> and <see cref="ReplaceErrors"/>.</summary>
|
||||
Default = default,
|
||||
|
||||
/// <summary>Enumerate as UTF-8 (the default.)</summary>
|
||||
Utf8 = Default,
|
||||
|
||||
/// <summary>Enumerate as UTF-8 in a SeString.</summary>
|
||||
Utf8SeString = 1 << 1,
|
||||
|
||||
/// <summary>Enumerate as UTF-16.</summary>
|
||||
Utf16 = 1 << 2,
|
||||
|
||||
/// <summary>Enumerate as UTF-32.</summary>
|
||||
Utf32 = 1 << 3,
|
||||
|
||||
/// <summary>Bitmask for specifying the encoding.</summary>
|
||||
UtfMask = Utf8 | Utf8SeString | Utf16 | Utf32,
|
||||
|
||||
/// <summary>On error, replace to U+FFFD (REPLACEMENT CHARACTER, the default.)</summary>
|
||||
ReplaceErrors = Default,
|
||||
|
||||
/// <summary>On error, drop the invalid byte.</summary>
|
||||
IgnoreErrors = 1 << 4,
|
||||
|
||||
/// <summary>On error, stop the handling.</summary>
|
||||
TerminateOnFirstError = 1 << 5,
|
||||
|
||||
/// <summary>On error, throw an exception.</summary>
|
||||
ThrowOnFirstError = 1 << 6,
|
||||
|
||||
/// <summary>Bitmask for specifying the error handling mode.</summary>
|
||||
ErrorHandlingMask = ReplaceErrors | IgnoreErrors | TerminateOnFirstError | ThrowOnFirstError,
|
||||
|
||||
/// <summary>Use the current system native endianness from <see cref="BitConverter.IsLittleEndian"/>
|
||||
/// (the default.)</summary>
|
||||
NativeEndian = Default,
|
||||
|
||||
/// <summary>Use little endianness.</summary>
|
||||
LittleEndian = 1 << 7,
|
||||
|
||||
/// <summary>Use big endianness.</summary>
|
||||
BigEndian = 1 << 8,
|
||||
|
||||
/// <summary>Bitmask for specifying endianness.</summary>
|
||||
EndiannessMask = NativeEndian | LittleEndian | BigEndian,
|
||||
|
||||
/// <summary>Disrespect byte order mask.</summary>
|
||||
DisrespectByteOrderMask = 1 << 9,
|
||||
|
||||
/// <summary>Yield byte order masks, if it shows up.</summary>
|
||||
YieldByteOrderMask = 1 << 10,
|
||||
}
|
||||
|
|
@ -0,0 +1,665 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
|
||||
/// <summary>Represents a single value to be used in a UTF-N byte sequence.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 4)]
|
||||
[DebuggerDisplay("0x{IntValue,h} ({CharValue})")]
|
||||
internal readonly struct UtfValue : IEquatable<UtfValue>, IComparable<UtfValue>
|
||||
{
|
||||
/// <summary>The unicode codepoint in <c>int</c>, that may not be in a valid range.</summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly int IntValue;
|
||||
|
||||
/// <summary>The unicode codepoint in <c>uint</c>, that may not be in a valid range.</summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly uint UIntValue;
|
||||
|
||||
/// <summary>The high UInt16 value in <c>char</c>, that may have been cut off if outside BMP.</summary>
|
||||
[FieldOffset(0)]
|
||||
public readonly char CharValue;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="UtfValue"/> struct.</summary>
|
||||
/// <param name="value">The raw codepoint value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public UtfValue(uint value) => this.UIntValue = value;
|
||||
|
||||
/// <inheritdoc cref="UtfValue(uint)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public UtfValue(int value) => this.IntValue = value;
|
||||
|
||||
/// <summary>Gets the length of this codepoint, encoded in UTF-8.</summary>
|
||||
public int Length8
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => GetEncodedLength8(this);
|
||||
}
|
||||
|
||||
/// <summary>Gets the length of this codepoint, encoded in UTF-16.</summary>
|
||||
public int Length16
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => GetEncodedLength16(this);
|
||||
}
|
||||
|
||||
/// <summary>Gets the short name, if supported.</summary>
|
||||
/// <returns>The buffer containing the short name, or empty if unsupported.</returns>
|
||||
public ReadOnlySpan<char> ShortName
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => GetShortName(this);
|
||||
}
|
||||
|
||||
public static implicit operator uint(UtfValue c) => c.UIntValue;
|
||||
|
||||
public static implicit operator int(UtfValue c) => c.IntValue;
|
||||
|
||||
public static implicit operator UtfValue(byte c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(sbyte c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(ushort c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(short c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(uint c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(int c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(char c) => new(c);
|
||||
|
||||
public static implicit operator UtfValue(Rune c) => new(c.Value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(UtfValue left, UtfValue right) => left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(UtfValue left, UtfValue right) => !left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator <(UtfValue left, UtfValue right) => left.CompareTo(right) < 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >(UtfValue left, UtfValue right) => left.CompareTo(right) > 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator <=(UtfValue left, UtfValue right) => left.CompareTo(right) <= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >=(UtfValue left, UtfValue right) => left.CompareTo(right) >= 0;
|
||||
|
||||
/// <summary>Gets the short name of the codepoint, for some select codepoints.</summary>
|
||||
/// <param name="codepoint">The codepoint.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public static ReadOnlySpan<char> GetShortName(int codepoint) =>
|
||||
codepoint switch
|
||||
{
|
||||
0x00 => "NUL",
|
||||
0x01 => "SOH",
|
||||
0x02 => "STX",
|
||||
0x03 => "ETX",
|
||||
0x04 => "EOT",
|
||||
0x05 => "ENQ",
|
||||
0x06 => "ACK",
|
||||
0x07 => "BEL",
|
||||
0x08 => "BS",
|
||||
0x09 => "HT",
|
||||
0x0a => "LF",
|
||||
0x0b => "VT",
|
||||
0x0c => "FF",
|
||||
0x0d => "CR",
|
||||
0x0e => "SO",
|
||||
0x0f => "SI",
|
||||
|
||||
0x10 => "DLE",
|
||||
0x11 => "DC1",
|
||||
0x12 => "DC2",
|
||||
0x13 => "DC3",
|
||||
0x14 => "DC4",
|
||||
0x15 => "NAK",
|
||||
0x16 => "SYN",
|
||||
0x17 => "SOH",
|
||||
0x18 => "CAN",
|
||||
0x19 => "EOM",
|
||||
0x1a => "SUB",
|
||||
0x1b => "ESC",
|
||||
0x1c => "FS",
|
||||
0x1d => "GS",
|
||||
0x1e => "RS",
|
||||
0x1f => "US",
|
||||
|
||||
0x80 => "PAD",
|
||||
0x81 => "HOP",
|
||||
0x82 => "BPH",
|
||||
0x83 => "NBH",
|
||||
0x84 => "IND",
|
||||
0x85 => "NEL",
|
||||
0x86 => "SSA",
|
||||
0x87 => "ESA",
|
||||
0x88 => "HTS",
|
||||
0x89 => "HTJ",
|
||||
0x8a => "VTS",
|
||||
0x8b => "PLD",
|
||||
0x8c => "PLU",
|
||||
0x8d => "RI",
|
||||
0x8e => "SS2",
|
||||
0x8f => "SS3",
|
||||
|
||||
0x90 => "DCS",
|
||||
0x91 => "PU1",
|
||||
0x92 => "PU2",
|
||||
0x93 => "STS",
|
||||
0x94 => "CCH",
|
||||
0x95 => "MW",
|
||||
0x96 => "SPA",
|
||||
0x97 => "EPA",
|
||||
0x98 => "SOS",
|
||||
0x99 => "SGC",
|
||||
0x9a => "SCI",
|
||||
0x9b => "CSI",
|
||||
0x9c => "ST",
|
||||
0x9d => "OSC",
|
||||
0x9e => "PM",
|
||||
0x9f => "APC",
|
||||
|
||||
0xa0 => "NBSP",
|
||||
0xad => "SHY",
|
||||
|
||||
_ => default,
|
||||
};
|
||||
|
||||
/// <summary>Gets the length of the codepoint, when encoded in UTF-8.</summary>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <returns>The length.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetEncodedLength8(int codepoint) => (uint)codepoint switch
|
||||
{
|
||||
< 1u << 7 => 1,
|
||||
< 1u << 11 => 2,
|
||||
< 1u << 16 => 3,
|
||||
< 1u << 21 => 4,
|
||||
// Not a valid Unicode codepoint anymore below.
|
||||
< 1u << 26 => 5,
|
||||
< 1u << 31 => 6,
|
||||
_ => 7,
|
||||
};
|
||||
|
||||
/// <summary>Gets the length of the codepoint, when encoded in UTF-16.</summary>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <returns>The length.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetEncodedLength16(int codepoint) => (uint)codepoint switch
|
||||
{
|
||||
< 0x10000 => 2,
|
||||
< 0x10000 + (1 << 20) => 4,
|
||||
// Not a valid Unicode codepoint anymore below.
|
||||
< 0x10000 + (1 << 30) => 6,
|
||||
_ => 8,
|
||||
};
|
||||
|
||||
/// <inheritdoc cref="TryDecode8(ReadOnlySpan{byte}, out UtfValue, out int)"/>
|
||||
/// <remarks>Trims <paramref name="source"/> at beginning by <paramref name="length"/>.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryDecode8(ref ReadOnlySpan<byte> source, out UtfValue value, out int length)
|
||||
{
|
||||
var v = TryDecode8(source, out value, out length);
|
||||
source = source[length..];
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to decode a value from a UTF-8 byte sequence.</summary>
|
||||
/// <param name="source">The span to decode from.</param>
|
||||
/// <param name="value">The decoded value.</param>
|
||||
/// <param name="length">The length of the consumed bytes. <c>1</c> if sequence is broken.</param>
|
||||
/// <returns><c>true</c> if <paramref name="source"/> is successfully decoded.</returns>
|
||||
/// <remarks>Codepoints that results in <c>false</c> from <see cref="Rune.IsValid(int)"/> can still be returned,
|
||||
/// including unpaired surrogate characters, or codepoints above U+10FFFFF. This function returns a value only
|
||||
/// indicating whether the sequence could be decoded into a number, without being too short.</remarks>
|
||||
public static unsafe bool TryDecode8(ReadOnlySpan<byte> source, out UtfValue value, out int length)
|
||||
{
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
value = default;
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = source)
|
||||
{
|
||||
if ((ptr[0] & 0x80) == 0)
|
||||
{
|
||||
length = 1;
|
||||
value = ptr[0];
|
||||
}
|
||||
else if ((ptr[0] & 0b11100000) == 0b11000000 && source.Length >= 2
|
||||
&& ((uint)ptr[1] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 2;
|
||||
value = (((uint)ptr[0] & 0x1F) << 6) |
|
||||
(((uint)ptr[1] & 0x3F) << 0);
|
||||
}
|
||||
else if (((uint)ptr[0] & 0b11110000) == 0b11100000 && source.Length >= 3
|
||||
&& ((uint)ptr[1] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[2] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 3;
|
||||
value = (((uint)ptr[0] & 0x0F) << 12) |
|
||||
(((uint)ptr[1] & 0x3F) << 6) |
|
||||
(((uint)ptr[2] & 0x3F) << 0);
|
||||
}
|
||||
else if (((uint)ptr[0] & 0b11111000) == 0b11110000 && source.Length >= 4
|
||||
&& ((uint)ptr[1] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[2] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[3] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 4;
|
||||
value = (((uint)ptr[0] & 0x07) << 18) |
|
||||
(((uint)ptr[1] & 0x3F) << 12) |
|
||||
(((uint)ptr[2] & 0x3F) << 6) |
|
||||
(((uint)ptr[3] & 0x3F) << 0);
|
||||
}
|
||||
else if (((uint)ptr[0] & 0b11111100) == 0b11111000 && source.Length >= 5
|
||||
&& ((uint)ptr[1] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[2] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[3] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[4] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 5;
|
||||
value = (((uint)ptr[0] & 0x03) << 24) |
|
||||
(((uint)ptr[1] & 0x3F) << 18) |
|
||||
(((uint)ptr[2] & 0x3F) << 12) |
|
||||
(((uint)ptr[3] & 0x3F) << 6) |
|
||||
(((uint)ptr[4] & 0x3F) << 0);
|
||||
}
|
||||
else if (((uint)ptr[0] & 0b11111110) == 0b11111100 && source.Length >= 6
|
||||
&& ((uint)ptr[1] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[2] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[3] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[4] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[5] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 6;
|
||||
value = (((uint)ptr[0] & 0x01) << 30) |
|
||||
(((uint)ptr[1] & 0x3F) << 24) |
|
||||
(((uint)ptr[2] & 0x3F) << 18) |
|
||||
(((uint)ptr[3] & 0x3F) << 12) |
|
||||
(((uint)ptr[4] & 0x3F) << 6) |
|
||||
(((uint)ptr[5] & 0x3F) << 0);
|
||||
}
|
||||
else if (((uint)ptr[0] & 0b11111111) == 0b11111110 && source.Length >= 7
|
||||
&& ((uint)ptr[1] & 0b11111100) == 0b10000000
|
||||
&& ((uint)ptr[2] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[3] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[4] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[5] & 0b11000000) == 0b10000000
|
||||
&& ((uint)ptr[6] & 0b11000000) == 0b10000000)
|
||||
{
|
||||
length = 7;
|
||||
value = (((uint)ptr[1] & 0x03) << 30) |
|
||||
(((uint)ptr[2] & 0x3F) << 24) |
|
||||
(((uint)ptr[3] & 0x3F) << 18) |
|
||||
(((uint)ptr[4] & 0x3F) << 12) |
|
||||
(((uint)ptr[5] & 0x3F) << 6) |
|
||||
(((uint)ptr[6] & 0x3F) << 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
length = 1;
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryDecode16(ReadOnlySpan{byte}, bool, out UtfValue, out int)"/>
|
||||
/// <remarks>Trims <paramref name="source"/> at beginning by <paramref name="length"/>.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryDecode16(ref ReadOnlySpan<byte> source, bool be, out UtfValue value, out int length)
|
||||
{
|
||||
var v = TryDecode16(source, be, out value, out length);
|
||||
source = source[length..];
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to decode a value from a UTF-16 byte sequence.</summary>
|
||||
/// <param name="source">The span to decode from.</param>
|
||||
/// <param name="be">Whether to use big endian.</param>
|
||||
/// <param name="value">The decoded value.</param>
|
||||
/// <param name="length">The length of the consumed bytes. <c>1</c> if cut short.
|
||||
/// <c>2</c> if sequence is broken.</param>
|
||||
/// <returns><c>true</c> if <paramref name="source"/> is successfully decoded.</returns>
|
||||
/// <remarks>Codepoints that results in <c>false</c> from <see cref="Rune.IsValid(int)"/> can still be returned,
|
||||
/// including unpaired surrogate characters, or codepoints above U+10FFFFF. This function returns a value only
|
||||
/// indicating whether the sequence could be decoded into a number, without being too short.</remarks>
|
||||
public static unsafe bool TryDecode16(ReadOnlySpan<byte> source, bool be, out UtfValue value, out int length)
|
||||
{
|
||||
if (source.Length < 2)
|
||||
{
|
||||
value = default;
|
||||
length = source.Length;
|
||||
return false;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = source)
|
||||
{
|
||||
var p16 = (ushort*)ptr;
|
||||
var val = be == BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(*p16) : *p16;
|
||||
if (char.IsHighSurrogate((char)val))
|
||||
{
|
||||
var lookahead1 = source.Length >= 4 ? p16[1] : 0;
|
||||
var lookahead2 = source.Length >= 6 ? p16[2] : 0;
|
||||
var lookahead3 = source.Length >= 8 ? p16[3] : 0;
|
||||
if (char.IsLowSurrogate((char)lookahead1))
|
||||
{
|
||||
// Not a valid Unicode codepoint anymore inside the block below.
|
||||
if (char.IsLowSurrogate((char)lookahead2))
|
||||
{
|
||||
if (char.IsLowSurrogate((char)lookahead3))
|
||||
{
|
||||
value = 0x10000
|
||||
+ (((val & 0x3) << 30) |
|
||||
((lookahead1 & 0x3FF) << 20) |
|
||||
((lookahead2 & 0x3FF) << 10) |
|
||||
((lookahead3 & 0x3FF) << 0));
|
||||
length = 8;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = 0x10000
|
||||
+ (((val & 0x3FF) << 20) |
|
||||
((lookahead1 & 0x3FF) << 10) |
|
||||
((lookahead2 & 0x3FF) << 0));
|
||||
length = 6;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = 0x10000 +
|
||||
(((val & 0x3FF) << 10) |
|
||||
((lookahead1 & 0x3FF) << 0));
|
||||
length = 4;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Calls are supposed to handle unpaired surrogates.
|
||||
value = val;
|
||||
length = 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryDecode32(ReadOnlySpan{byte}, bool, out UtfValue, out int)"/>
|
||||
/// <remarks>Trims <paramref name="source"/> at beginning by <paramref name="length"/>.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryDecode32(ref ReadOnlySpan<byte> source, bool be, out UtfValue value, out int length)
|
||||
{
|
||||
var v = TryDecode32(source, be, out value, out length);
|
||||
source = source[length..];
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to decode a value from a UTF-32 byte sequence.</summary>
|
||||
/// <param name="source">The span to decode from.</param>
|
||||
/// <param name="be">Whether to use big endian.</param>
|
||||
/// <param name="value">The decoded value.</param>
|
||||
/// <param name="length">The length of the consumed bytes. <c>1 to 3</c> if cut short.
|
||||
/// <c>4</c> if sequence is broken.</param>
|
||||
/// <returns><c>true</c> if <paramref name="source"/> is successfully decoded.</returns>
|
||||
/// <remarks>Codepoints that results in <c>false</c> from <see cref="Rune.IsValid(int)"/> can still be returned,
|
||||
/// including unpaired surrogate characters, or codepoints above U+10FFFFF. This function returns a value only
|
||||
/// indicating whether the sequence could be decoded into a number, without being too short.</remarks>
|
||||
public static bool TryDecode32(ReadOnlySpan<byte> source, bool be, out UtfValue value, out int length)
|
||||
{
|
||||
if (source.Length < 4)
|
||||
{
|
||||
value = default;
|
||||
length = source.Length;
|
||||
return false;
|
||||
}
|
||||
|
||||
length = 4;
|
||||
if ((be && BinaryPrimitives.TryReadInt32BigEndian(source, out var i32))
|
||||
|| (!be && BinaryPrimitives.TryReadInt32LittleEndian(source, out i32)))
|
||||
{
|
||||
value = i32;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-8.</summary>
|
||||
/// <param name="target">The target stream.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <returns>The length of the encoded data.</returns>
|
||||
/// <remarks>Trims <paramref name="target"/> at beginning by the length.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Encode8(Stream target, int codepoint)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[7];
|
||||
Encode8(buf, codepoint, out var length);
|
||||
target.Write(buf[..length]);
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-8.</summary>
|
||||
/// <param name="target">The target byte span.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <returns>The length of the encoded data.</returns>
|
||||
/// <remarks>Trims <paramref name="target"/> at beginning by the length.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Encode8(ref Span<byte> target, int codepoint)
|
||||
{
|
||||
target = Encode8(target, codepoint, out var length);
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-8.</summary>
|
||||
/// <param name="target">The optional target byte span.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <param name="length">The length of the encoded data.</param>
|
||||
/// <returns>The remaning region of <paramref name="target"/>.</returns>
|
||||
public static Span<byte> Encode8(Span<byte> target, int codepoint, out int length)
|
||||
{
|
||||
var value = (uint)codepoint;
|
||||
length = GetEncodedLength8(codepoint);
|
||||
if (target.IsEmpty)
|
||||
return target;
|
||||
|
||||
switch (length)
|
||||
{
|
||||
case 1:
|
||||
target[0] = (byte)value;
|
||||
return target[1..];
|
||||
case 2:
|
||||
target[0] = (byte)(0xC0 | ((value >> 6) & 0x1F));
|
||||
target[1] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[2..];
|
||||
case 3:
|
||||
target[0] = (byte)(0xE0 | ((value >> 12) & 0x0F));
|
||||
target[1] = (byte)(0x80 | ((value >> 6) & 0x3F));
|
||||
target[2] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[3..];
|
||||
case 4:
|
||||
target[0] = (byte)(0xF0 | ((value >> 18) & 0x07));
|
||||
target[1] = (byte)(0x80 | ((value >> 12) & 0x3F));
|
||||
target[2] = (byte)(0x80 | ((value >> 6) & 0x3F));
|
||||
target[3] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[4..];
|
||||
case 5:
|
||||
target[0] = (byte)(0xF8 | ((value >> 24) & 0x03));
|
||||
target[1] = (byte)(0x80 | ((value >> 18) & 0x3F));
|
||||
target[2] = (byte)(0x80 | ((value >> 12) & 0x3F));
|
||||
target[3] = (byte)(0x80 | ((value >> 6) & 0x3F));
|
||||
target[4] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[5..];
|
||||
case 6:
|
||||
target[0] = (byte)(0xFC | ((value >> 30) & 0x01));
|
||||
target[1] = (byte)(0x80 | ((value >> 24) & 0x3F));
|
||||
target[2] = (byte)(0x80 | ((value >> 18) & 0x3F));
|
||||
target[3] = (byte)(0x80 | ((value >> 12) & 0x3F));
|
||||
target[4] = (byte)(0x80 | ((value >> 6) & 0x3F));
|
||||
target[5] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[6..];
|
||||
case 7:
|
||||
target[0] = 0xFE;
|
||||
target[1] = (byte)(0x80 | ((value >> 30) & 0x03));
|
||||
target[2] = (byte)(0x80 | ((value >> 24) & 0x3F));
|
||||
target[3] = (byte)(0x80 | ((value >> 18) & 0x3F));
|
||||
target[4] = (byte)(0x80 | ((value >> 12) & 0x3F));
|
||||
target[5] = (byte)(0x80 | ((value >> 6) & 0x3F));
|
||||
target[6] = (byte)(0x80 | ((value >> 0) & 0x3F));
|
||||
return target[7..];
|
||||
default:
|
||||
Debug.Assert(false, $"{nameof(Length8)} property should have produced all possible cases.");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-16.</summary>
|
||||
/// <param name="target">The target stream.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <param name="be">Whether to use big endian.</param>
|
||||
/// <returns>The length of the encoded data.</returns>
|
||||
/// <remarks>Trims <paramref name="target"/> at beginning by the length.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Encode16(Stream target, int codepoint, bool be)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[8];
|
||||
Encode16(buf, codepoint, be, out var length);
|
||||
target.Write(buf[..length]);
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-16.</summary>
|
||||
/// <param name="target">The target byte span.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <param name="be">Whether to use big endian.</param>
|
||||
/// <returns>The length of the encoded data.</returns>
|
||||
/// <remarks>Trims <paramref name="target"/> at beginning by the length.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Encode16(ref Span<byte> target, int codepoint, bool be)
|
||||
{
|
||||
target = Encode16(target, codepoint, be, out var length);
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target in UTF-16.</summary>
|
||||
/// <param name="target">The optional target byte span.</param>
|
||||
/// <param name="codepoint">The codepoint to encode.</param>
|
||||
/// <param name="be">Whether to use big endian.</param>
|
||||
/// <param name="length">The length of the encoded data.</param>
|
||||
/// <returns>The remaning region of <paramref name="target"/>.</returns>
|
||||
public static Span<byte> Encode16(Span<byte> target, int codepoint, bool be, out int length)
|
||||
{
|
||||
var value = (uint)codepoint;
|
||||
length = GetEncodedLength16(codepoint);
|
||||
if (target.IsEmpty)
|
||||
return target;
|
||||
|
||||
if (be)
|
||||
{
|
||||
switch (length)
|
||||
{
|
||||
case 2:
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[0..], (ushort)value);
|
||||
return target[2..];
|
||||
case 4:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[0..], (ushort)(0xD800 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[2..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[4..];
|
||||
case 6:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[0..], (ushort)(0xD800 | ((value >> 20) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[2..], (ushort)(0xDC00 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[4..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[6..];
|
||||
case 8:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[0..], (ushort)(0xD800 | ((value >> 30) & 0x3)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[2..], (ushort)(0xDC00 | ((value >> 20) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[4..], (ushort)(0xDC00 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(target[6..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[8..];
|
||||
default:
|
||||
Debug.Assert(false, $"{nameof(Length16)} property should have produced all possible cases.");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
switch (length)
|
||||
{
|
||||
case 2:
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[0..], (ushort)value);
|
||||
return target[2..];
|
||||
case 4:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[0..], (ushort)(0xD800 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[2..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[4..];
|
||||
case 6:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[0..], (ushort)(0xD800 | ((value >> 20) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[2..], (ushort)(0xDC00 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[4..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[6..];
|
||||
case 8:
|
||||
value -= 0x10000;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[0..], (ushort)(0xD800 | ((value >> 30) & 0x3)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[2..], (ushort)(0xDC00 | ((value >> 20) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[4..], (ushort)(0xDC00 | ((value >> 10) & 0x3FF)));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(target[6..], (ushort)(0xDC00 | ((value >> 00) & 0x3FF)));
|
||||
return target[8..];
|
||||
default:
|
||||
Debug.Assert(false, $"{nameof(Length16)} property should have produced all possible cases.");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int CompareTo(UtfValue other) => this.IntValue.CompareTo(other.IntValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(UtfValue other) => this.IntValue == other.IntValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override bool Equals(object? obj) => obj is UtfValue other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode() => this.IntValue;
|
||||
|
||||
/// <summary>Attempts to get the corresponding rune.</summary>
|
||||
/// <param name="rune">The retrieved rune.</param>
|
||||
/// <returns><c>true</c> if retrieved.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetRune(out Rune rune)
|
||||
{
|
||||
if (Rune.IsValid(this.IntValue))
|
||||
{
|
||||
rune = new(this.IntValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
rune = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Encodes the codepoint to the target.</summary>
|
||||
/// <param name="target">The target byte span.</param>
|
||||
/// <returns>The remaning region of <paramref name="target"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> Encode8(Span<byte> target) => Encode8(target, this, out _);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,8 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ImGuiNET;
|
||||
|
|
@ -203,7 +202,9 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case NodeType.Text:
|
||||
var textNode = (AtkTextNode*)node;
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)textNode->NodeText.StringPtr)}");
|
||||
ImGui.Text("text: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textNode->NodeText);
|
||||
|
||||
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
|
||||
|
||||
|
|
@ -230,7 +231,9 @@ internal unsafe class UiDebug
|
|||
break;
|
||||
case NodeType.Counter:
|
||||
var counterNode = (AtkCounterNode*)node;
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)counterNode->NodeText.StringPtr)}");
|
||||
ImGui.Text("text: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(counterNode->NodeText);
|
||||
break;
|
||||
case NodeType.Image:
|
||||
var imageNode = (AtkImageNode*)node;
|
||||
|
|
@ -272,13 +275,8 @@ internal unsafe class UiDebug
|
|||
$"texture type: {texType} part_id={partId} part_id_count={partsList->PartCount}");
|
||||
if (texType == TextureType.Resource)
|
||||
{
|
||||
var texFileNameStdString =
|
||||
&textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
|
||||
var texString = texFileNameStdString->Length < 16
|
||||
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
|
||||
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
|
||||
|
||||
ImGui.Text($"texture path: {texString}");
|
||||
ImGui.Text(
|
||||
$"texture path: {textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName}");
|
||||
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
|
||||
|
||||
if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}"))
|
||||
|
|
@ -372,13 +370,33 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case ComponentType.TextInput:
|
||||
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
|
||||
ImGui.Text($"InputBase Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText01.StringPtr))}");
|
||||
ImGui.Text($"Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText02.StringPtr))}");
|
||||
ImGui.Text($"Text3: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText03.StringPtr))}");
|
||||
ImGui.Text($"Text4: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText04.StringPtr))}");
|
||||
ImGui.Text($"Text5: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText05.StringPtr))}");
|
||||
ImGui.Text("InputBase Text1: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText1);
|
||||
|
||||
ImGui.Text("InputBase Text2: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText2);
|
||||
|
||||
ImGui.Text("Text1: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText01);
|
||||
|
||||
ImGui.Text("Text2: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText02);
|
||||
|
||||
ImGui.Text("Text3: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText03);
|
||||
|
||||
ImGui.Text("Text4: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText04);
|
||||
|
||||
ImGui.Text("Text5: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText05);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal class DataWindow : Window, IDisposable
|
|||
new PluginIpcWidget(),
|
||||
new SeFontTestWidget(),
|
||||
new ServicesWidget(),
|
||||
new SeStringRendererTestWidget(),
|
||||
new StartInfoWidget(),
|
||||
new TargetWidget(),
|
||||
new TaskSchedulerWidget(),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -16,6 +16,10 @@ using Dalamud.Interface.Utility.Raii;
|
|||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using SeStringRenderer = Dalamud.Interface.Internal.ImGuiSeStringRenderer.SeStringRenderer;
|
||||
|
||||
namespace Dalamud.Interface.Utility;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -177,6 +181,14 @@ public static class ImGuiHelpers
|
|||
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}");
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SeStringRenderer.DrawWrapped(ReadOnlySeStringSpan, float)"/>
|
||||
public static void SeStringWrapped(ReadOnlySpan<byte> sss, float wrapWidth = 0) =>
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(sss, wrapWidth);
|
||||
|
||||
/// <inheritdoc cref="SeStringRenderer.CompileAndDrawWrapped"/>
|
||||
public static void CompileSeStringWrapped(string text, float wrapWidth = 0) =>
|
||||
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(text, wrapWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Write unformatted text wrapped.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,15 @@ public interface IGameConfig
|
|||
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
|
||||
/// <returns>A value representing the success.</returns>
|
||||
public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get a string config value as a gamepad button enum value from the UiConfig section.
|
||||
/// </summary>
|
||||
/// <param name="option">Option to get the value of.</param>
|
||||
/// <param name="value">The returned value of the config option.</param>
|
||||
/// <returns>A value representing the success.</returns>
|
||||
public bool TryGet(SystemConfigOption option, out PadButtonValue value);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the properties of a String option from the System section.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue