diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index c8cce8a8ce..38a5ad345f 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -114,6 +114,13 @@
+
+
+
+
+
+
+
diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs
index bfb58fd3ca..9579d84bc6 100644
--- a/Dalamud/Game/Config/GameConfig.cs
+++ b/Dalamud/Game/Config/GameConfig.cs
@@ -121,7 +121,10 @@ private unsafe GameConfig(Framework framework, TargetSigScanner sigScanner)
///
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
-
+
+ ///
+ public bool TryGet(SystemConfigOption option, out PadButtonValue value) => this.System.TryGetStringAsEnum(option.GetName(), out value);
+
///
public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value);
@@ -346,7 +349,11 @@ public bool TryGet(SystemConfigOption option, out FloatConfigProperties? propert
///
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
-
+
+ ///
+ public bool TryGet(SystemConfigOption option, out PadButtonValue value)
+ => this.gameConfigService.TryGet(option, out value);
+
///
public bool TryGet(UiConfigOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value);
diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs
index 31e4a0b3f8..9cd239d84e 100644
--- a/Dalamud/Game/Config/GameConfigSection.cs
+++ b/Dalamud/Game/Config/GameConfigSection.cs
@@ -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 string GetString(string name)
return value;
}
+ /// Attempts to get a string config value as an enum value.
+ /// Name of the config option.
+ /// The returned value of the config option.
+ /// Type of the enum. Name of each enum fields are compared against.
+ /// A value representing the success.
+ public unsafe bool TryGetStringAsEnum(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 n16 = stackalloc char[Encoding.UTF8.GetCharCount(n8)];
+ Encoding.UTF8.GetChars(n8, n16);
+ return Enum.TryParse(n16, out value);
+ }
+
///
/// Set a string config option.
/// Note: Not all config options will be be immediately reflected in the game.
diff --git a/Dalamud/Game/Config/PadButtonValue.cs b/Dalamud/Game/Config/PadButtonValue.cs
new file mode 100644
index 0000000000..bd6da48bb1
--- /dev/null
+++ b/Dalamud/Game/Config/PadButtonValue.cs
@@ -0,0 +1,85 @@
+namespace Dalamud.Game.Config;
+
+// ReSharper disable InconsistentNaming
+// ReSharper disable IdentifierTypo
+// ReSharper disable CommentTypo
+
+/// Valid values for PadButton options under .
+/// Names are the valid part. Enum values are exclusively for use with current Dalamud version.
+public enum PadButtonValue
+{
+ /// Auto-run.
+ Autorun_Support,
+
+ /// Change Hotbar Set.
+ Hotbar_Set_Change,
+
+ /// Highlight Left Hotbar.
+ XHB_Left_Start,
+
+ /// Highlight Right Hotbar.
+ XHB_Right_Start,
+
+ /// Not directly referenced by Gamepad button customization window.
+ Cursor_Operation,
+
+ /// Draw Weapon/Lock On.
+ Lockon_and_Sword,
+
+ /// Sit/Lock On.
+ Lockon_and_Sit,
+
+ /// Change Camera.
+ Camera_Modechange,
+
+ /// Reset Camera Position.
+ Camera_Reset,
+
+ /// Draw/Sheathe Weapon.
+ Drawn_Sword,
+
+ /// Lock On.
+ Camera_Lockononly,
+
+ /// Face Target.
+ FaceTarget,
+
+ /// Assist Target.
+ AssistTarget,
+
+ /// Face Camera.
+ LookCamera,
+
+ /// Execute Macro #98 (Exclusive).
+ Macro98,
+
+ /// Execute Macro #99 (Exclusive).
+ Macro99,
+
+ /// Not Assigned.
+ Notset,
+
+ /// Jump/Cancel Casting.
+ Jump,
+
+ /// Select Target/Confirm.
+ Accept,
+
+ /// Cancel.
+ Cancel,
+
+ /// Open Map/Subcommands.
+ Map_Sub,
+
+ /// Open Main Menu.
+ MainCommand,
+
+ /// Select HUD.
+ HUD_Select,
+
+ /// Move Character.
+ Move_Operation,
+
+ /// Move Camera.
+ Camera_Operation,
+}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
new file mode 100644
index 0000000000..2083d9cd98
--- /dev/null
+++ b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
@@ -0,0 +1,149 @@
+using System.IO;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+using Lumina.Data;
+
+namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
+
+/// Reference member view of a .gfd file data.
+internal sealed unsafe class GfdFile : FileResource
+{
+ /// Gets or sets the file header.
+ public GfdHeader Header { get; set; }
+
+ /// Gets or sets the entries.
+ public GfdEntry[] Entries { get; set; } = [];
+
+ ///
+ 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(this.DataSpan);
+ this.Entries = MemoryMarshal.Cast(this.DataSpan[sizeof(GfdHeader)..]).ToArray();
+ }
+
+ /// Attempts to get an entry.
+ /// The icon ID.
+ /// The entry.
+ /// Whether to follow redirects.
+ /// true if found.
+ 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;
+ }
+
+ /// Header of a .gfd file.
+ [StructLayout(LayoutKind.Sequential)]
+ public struct GfdHeader
+ {
+ /// Signature: "gftd0100".
+ public fixed byte Signature[8];
+
+ /// Number of entries.
+ public int Count;
+
+ /// Unused/unknown.
+ public fixed byte Padding[4];
+ }
+
+ /// An entry of a .gfd file.
+ [StructLayout(LayoutKind.Sequential, Size = 0x10)]
+ public struct GfdEntry
+ {
+ /// ID of the entry.
+ public ushort Id;
+
+ /// The left offset of the entry.
+ public ushort Left;
+
+ /// The top offset of the entry.
+ public ushort Top;
+
+ /// The width of the entry.
+ public ushort Width;
+
+ /// The height of the entry.
+ public ushort Height;
+
+ /// Unknown/unused.
+ public ushort Unk0A;
+
+ /// The redirected entry, maybe.
+ public ushort Redirect;
+
+ /// Unknown/unused.
+ public ushort Unk0E;
+
+ /// Gets a value indicating whether this entry is effectively empty.
+ public bool IsEmpty => this.Width == 0 || this.Height == 0;
+
+ /// Gets or sets the size of this entry.
+ public Vector2 Size
+ {
+ get => new(this.Width, this.Height);
+ set => (this.Width, this.Height) = (checked((ushort)value.X), checked((ushort)value.Y));
+ }
+
+ /// Gets the UV0 of this entry.
+ public Vector2 Uv0 => new(this.Left / 512f, this.Top / 1024f);
+
+ /// Gets the UV1 of this entry.
+ public Vector2 Uv1 => new((this.Left + this.Width) / 512f, (this.Top + this.Height) / 1024f);
+
+ /// Gets the UV0 of the HQ version of this entry.
+ public Vector2 HqUv0 => new(this.Left / 256f, (this.Top + 170.5f) / 512f);
+
+ /// Gets the UV1 of the HQ version of this entry.
+ public Vector2 HqUv1 => new((this.Left + this.Width) / 256f, (this.Top + this.Height + 170.5f) / 512f);
+ }
+}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs
new file mode 100644
index 0000000000..face85cfcc
--- /dev/null
+++ b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs
@@ -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;
+
+/// Draws SeString.
+[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.Get();
+
+ private readonly ConcurrentLru cache = new(1024);
+
+ private readonly GfdFile gfd;
+ private readonly uint[] colorTypes;
+ private readonly uint[] edgeColorTypes;
+
+ private readonly List words = [];
+
+ private readonly List colorStack = [];
+ private readonly List edgeColorStack = [];
+ private readonly List 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()!;
+ 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("common/font/gfdata.gfd")!;
+
+ return;
+
+ static uint BgraToRgba(uint x)
+ {
+ var buf = (byte*)&x;
+ (buf[0], buf[2]) = (buf[2], buf[0]);
+ return x;
+ }
+ }
+
+ /// Finalizes an instance of the class.
+ ~SeStringRenderer() => this.ReleaseUnmanagedResources();
+
+ ///
+ void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
+
+ /// Creates and caches a SeString from a text macro representation, and then draws it.
+ /// SeString text macro representation.
+ /// Wrapping width. If a non-positive number is provided, then the remainder of the width
+ /// will be used.
+ 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("
"));
+ var res = new ReadOnlySeString(outstr.AsSpan().ToArray());
+ outstr.Dtor();
+ return res;
+ }).AsSpan(),
+ wrapWidth);
+ }
+
+ ///
+ public void DrawWrapped(in Utf8String utf8String, float wrapWidth = 0) =>
+ this.DrawWrapped(utf8String.AsSpan(), wrapWidth);
+
+ /// Draws a SeString.
+ /// SeString to draw.
+ /// Wrapping width. If a non-positive number is provided, then the remainder of the width
+ /// will be used.
+ 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);
+ }
+
+ /// Gets the printable char for the given char, or null(\0) if it should not be handled at all.
+ /// Character to determine.
+ /// Character to print, or null(\0) if none.
+ 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 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 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 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 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);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/DerivedGeneralCategory.txt b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/DerivedGeneralCategory.txt
new file mode 100644
index 0000000000..285ffa8fb8
--- /dev/null
+++ b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/DerivedGeneralCategory.txt
@@ -0,0 +1,4233 @@
+# DerivedGeneralCategory-15.1.0.txt
+# Date: 2023-07-28, 23:34:02 GMT
+# © 2023 Unicode®, Inc.
+# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
+# For terms of use, see https://www.unicode.org/terms_of_use.html
+#
+# Unicode Character Database
+# For documentation, see https://www.unicode.org/reports/tr44/
+
+# ================================================
+
+# Property: General_Category
+
+# ================================================
+
+# General_Category=Unassigned
+
+0378..0379 ; Cn # [2] ..
+0380..0383 ; Cn # [4] ..
+038B ; Cn #
+038D ; Cn #
+03A2 ; Cn #
+0530 ; Cn #
+0557..0558 ; Cn # [2] ..
+058B..058C ; Cn # [2] ..
+0590 ; Cn #
+05C8..05CF ; Cn # [8] ..
+05EB..05EE ; Cn # [4] ..
+05F5..05FF ; Cn # [11] ..
+070E ; Cn #
+074B..074C ; Cn # [2] ..
+07B2..07BF ; Cn # [14] ..
+07FB..07FC ; Cn # [2] ..
+082E..082F ; Cn # [2] ..
+083F ; Cn #
+085C..085D ; Cn # [2] ..
+085F ; Cn #
+086B..086F ; Cn # [5] ..
+088F ; Cn #
+0892..0897 ; Cn # [6] ..
+0984 ; Cn #
+098D..098E ; Cn # [2] ..
+0991..0992 ; Cn # [2] ..
+09A9 ; Cn #
+09B1 ; Cn #
+09B3..09B5 ; Cn # [3] ..
+09BA..09BB ; Cn # [2] ..
+09C5..09C6 ; Cn # [2] ..
+09C9..09CA ; Cn # [2] ..
+09CF..09D6 ; Cn # [8] ..
+09D8..09DB ; Cn # [4] ..
+09DE ; Cn #
+09E4..09E5 ; Cn # [2] ..
+09FF..0A00 ; Cn # [2] ..
+0A04 ; Cn #
+0A0B..0A0E ; Cn # [4] ..
+0A11..0A12 ; Cn # [2] ..
+0A29 ; Cn #
+0A31 ; Cn #
+0A34 ; Cn #
+0A37 ; Cn #
+0A3A..0A3B ; Cn # [2] ..
+0A3D ; Cn #
+0A43..0A46 ; Cn # [4] ..
+0A49..0A4A ; Cn # [2] ..
+0A4E..0A50 ; Cn # [3] ..
+0A52..0A58 ; Cn # [7] ..
+0A5D ; Cn #
+0A5F..0A65 ; Cn # [7] ..
+0A77..0A80 ; Cn # [10] ..
+0A84 ; Cn #
+0A8E ; Cn #
+0A92 ; Cn #
+0AA9 ; Cn #
+0AB1 ; Cn #
+0AB4 ; Cn #
+0ABA..0ABB ; Cn # [2] ..
+0AC6 ; Cn #
+0ACA ; Cn #
+0ACE..0ACF ; Cn # [2] ..
+0AD1..0ADF ; Cn # [15] ..
+0AE4..0AE5 ; Cn # [2] ..
+0AF2..0AF8 ; Cn # [7] ..
+0B00 ; Cn #
+0B04 ; Cn #
+0B0D..0B0E ; Cn # [2] ..
+0B11..0B12 ; Cn # [2] ..
+0B29 ; Cn #
+0B31 ; Cn #
+0B34 ; Cn #
+0B3A..0B3B ; Cn # [2] ..
+0B45..0B46 ; Cn # [2] ..
+0B49..0B4A ; Cn # [2] ..
+0B4E..0B54 ; Cn # [7] ..
+0B58..0B5B ; Cn # [4] ..
+0B5E ; Cn #
+0B64..0B65 ; Cn # [2] ..
+0B78..0B81 ; Cn # [10] ..
+0B84 ; Cn #
+0B8B..0B8D ; Cn # [3] ..
+0B91 ; Cn #
+0B96..0B98 ; Cn # [3] ..
+0B9B ; Cn #
+0B9D ; Cn #
+0BA0..0BA2 ; Cn # [3] ..
+0BA5..0BA7 ; Cn # [3] ..
+0BAB..0BAD ; Cn # [3] ..
+0BBA..0BBD ; Cn # [4] ..
+0BC3..0BC5 ; Cn # [3] ..
+0BC9 ; Cn #
+0BCE..0BCF ; Cn # [2] ..
+0BD1..0BD6 ; Cn # [6] ..
+0BD8..0BE5 ; Cn # [14] ..
+0BFB..0BFF ; Cn # [5] ..
+0C0D ; Cn #
+0C11 ; Cn #
+0C29 ; Cn #
+0C3A..0C3B ; Cn # [2] ..
+0C45 ; Cn #
+0C49 ; Cn #
+0C4E..0C54 ; Cn # [7] ..
+0C57 ; Cn #
+0C5B..0C5C ; Cn # [2] ..
+0C5E..0C5F ; Cn # [2] ..
+0C64..0C65 ; Cn # [2] ..
+0C70..0C76 ; Cn # [7] ..
+0C8D ; Cn #
+0C91 ; Cn #
+0CA9 ; Cn #
+0CB4 ; Cn #
+0CBA..0CBB ; Cn # [2] ..
+0CC5 ; Cn #
+0CC9 ; Cn #
+0CCE..0CD4 ; Cn # [7] ..
+0CD7..0CDC ; Cn # [6] ..
+0CDF ; Cn #
+0CE4..0CE5 ; Cn # [2] ..
+0CF0 ; Cn #
+0CF4..0CFF ; Cn # [12] ..
+0D0D ; Cn #
+0D11 ; Cn #
+0D45 ; Cn #
+0D49 ; Cn #
+0D50..0D53 ; Cn # [4] ..
+0D64..0D65 ; Cn # [2] ..
+0D80 ; Cn #
+0D84 ; Cn #
+0D97..0D99 ; Cn # [3] ..
+0DB2 ; Cn #
+0DBC ; Cn #
+0DBE..0DBF ; Cn # [2] ..
+0DC7..0DC9 ; Cn # [3] ..
+0DCB..0DCE ; Cn # [4] ..
+0DD5 ; Cn #
+0DD7 ; Cn #
+0DE0..0DE5 ; Cn # [6] ..
+0DF0..0DF1 ; Cn # [2] ..
+0DF5..0E00 ; Cn # [12] ..
+0E3B..0E3E ; Cn # [4] ..
+0E5C..0E80 ; Cn # [37] ..
+0E83 ; Cn #
+0E85 ; Cn #
+0E8B ; Cn #
+0EA4 ; Cn #
+0EA6 ; Cn #
+0EBE..0EBF ; Cn # [2] ..
+0EC5 ; Cn #
+0EC7 ; Cn #
+0ECF ; Cn #
+0EDA..0EDB ; Cn # [2] ..
+0EE0..0EFF ; Cn # [32]