diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 1d0ee79a4..5aa87d963 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -115,10 +115,10 @@
-
-
-
-
+
+
+
+
diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs
index d069be30d..cbe416625 100644
--- a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs
+++ b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs
@@ -3,6 +3,8 @@ using System.IO;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
+using Newtonsoft.Json;
+
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
///
@@ -13,32 +15,39 @@ public class DalamudLinkPayload : Payload
///
public override PayloadType Type => PayloadType.DalamudLink;
- ///
- /// Gets the plugin command ID to be linked.
- ///
- public uint CommandId { get; internal set; } = 0;
+ /// Gets the plugin command ID to be linked.
+ public uint CommandId { get; internal set; }
- ///
- /// Gets the plugin name to be linked.
- ///
+ /// Gets an optional extra integer value 1.
+ public int Extra1 { get; internal set; }
+
+ /// Gets an optional extra integer value 2.
+ public int Extra2 { get; internal set; }
+
+ /// Gets the plugin name to be linked.
public string Plugin { get; internal set; } = string.Empty;
+ /// Gets an optional extra string.
+ public string ExtraString { get; internal set; } = string.Empty;
+
///
- public override string ToString()
- {
- return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
- }
+ public override string ToString() =>
+ $"{this.Type} - {this.Plugin} ({this.CommandId}/{this.Extra1}/{this.Extra2}/{this.ExtraString})";
///
protected override byte[] EncodeImpl()
{
return new Lumina.Text.SeStringBuilder()
- .BeginMacro(MacroCode.Link)
- .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
- .AppendStringExpression(this.Plugin)
- .AppendUIntExpression(this.CommandId)
- .EndMacro()
- .ToArray();
+ .BeginMacro(MacroCode.Link)
+ .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
+ .AppendUIntExpression(this.CommandId)
+ .AppendIntExpression(this.Extra1)
+ .AppendIntExpression(this.Extra2)
+ .BeginStringExpression()
+ .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString }))
+ .EndExpression()
+ .EndMacro()
+ .ToArray();
}
///
@@ -49,16 +58,53 @@ public class DalamudLinkPayload : Payload
var body = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
var rosps = new ReadOnlySePayloadSpan(ReadOnlySePayloadType.Macro, MacroCode.Link, body.AsSpan());
- if (!rosps.TryGetExpression(out var pluginExpression, out var commandIdExpression))
- return;
+ if (!rosps.TryGetExpression(
+ out var commandIdExpression,
+ out var extra1Expression,
+ out var extra2Expression,
+ out var compositeExpression))
+ {
+ if (!rosps.TryGetExpression(out var pluginExpression, out commandIdExpression))
+ return;
- if (!pluginExpression.TryGetString(out var pluginString))
- return;
+ if (!pluginExpression.TryGetString(out var pluginString))
+ return;
- if (!commandIdExpression.TryGetUInt(out var commandId))
- return;
+ if (!commandIdExpression.TryGetUInt(out var commandId))
+ return;
- this.Plugin = pluginString.ExtractText();
- this.CommandId = commandId;
+ this.Plugin = pluginString.ExtractText();
+ this.CommandId = commandId;
+ }
+ else
+ {
+ if (!commandIdExpression.TryGetUInt(out var commandId))
+ return;
+
+ if (!extra1Expression.TryGetInt(out var extra1))
+ return;
+
+ if (!extra2Expression.TryGetInt(out var extra2))
+ return;
+
+ if (!compositeExpression.TryGetString(out var compositeString))
+ return;
+
+ string[] extraData;
+ try
+ {
+ extraData = JsonConvert.DeserializeObject(compositeString.ExtractText());
+ }
+ catch
+ {
+ return;
+ }
+
+ this.CommandId = commandId;
+ this.Extra1 = extra1;
+ this.Extra2 = extra2;
+ this.Plugin = extraData[0];
+ this.ExtraString = extraData[1];
+ }
}
}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
similarity index 98%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
index 48f9ad3a9..194d71957 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/GfdFile.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
using Lumina.Data;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// Game font data file.
internal sealed unsafe class GfdFile : FileResource
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
new file mode 100644
index 000000000..33fcb4496
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -0,0 +1,1149 @@
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using BitFaster.Caching.Lru;
+
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.Config;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Interface.ImGuiSeStringRenderer.Internal.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 FFXIVClientStructs.FFXIV.Component.Text;
+
+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;
+
+using SeStringBuilder = Lumina.Text.SeStringBuilder;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
+
+/// Draws SeString.
+[ServiceManager.EarlyLoadedService]
+internal unsafe class SeStringRenderer : IInternalDisposableService
+{
+ private const int ChannelLinkBackground = 0;
+ private const int ChannelShadow = 1;
+ private const int ChannelLinkUnderline = 2;
+ private const int ChannelEdge = 3;
+ private const int ChannelFore = 4;
+ private const int ChannelCount = 5;
+
+ private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
+ private const int ImGuiWindowDcOffset = 0x118;
+ private const int ImGuiWindowTempDataCurrLineTextBaseOffset = 0x38;
+
+ /// Soft hyphen character, which signifies that a word can be broken here, and will display a standard
+ /// hyphen when it is broken there.
+ private const int SoftHyphen = '\u00AD';
+
+ /// Object replacement character, which signifies that there should be something else displayed in place
+ /// of this placeholder. On its own, usually displayed like [OBJ].
+ private const int ObjectReplacementCharacter = '\uFFFC';
+
+ /// SeString to return instead, if macro encoder has failed and could not provide us the reason.
+ private static readonly ReadOnlySeString MacroEncoderEncodeStringError =
+ new SeStringBuilder()
+ .BeginMacro(MacroCode.ColorType).AppendIntExpression(508).EndMacro()
+ .BeginMacro(MacroCode.EdgeColorType).AppendIntExpression(509).EndMacro()
+ .Append(
+ ""u8)
+ .BeginMacro(MacroCode.EdgeColorType).AppendIntExpression(0).EndMacro()
+ .BeginMacro(MacroCode.ColorType).AppendIntExpression(0).EndMacro()
+ .ToReadOnlySeString();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameConfig gameConfig = Service.Get();
+
+ /// Cache of compiled SeStrings from .
+ private readonly ConcurrentLru cache = new(1024);
+
+ /// Sets the global invalid parameter handler. Used to suppress vsprintf_s from raising.
+ /// There exists a thread local version of this, but as the game-provided implementation is what
+ /// effectively is a screaming tool that the game has a bug, it should be safe to fail in any means.
+ private readonly delegate* unmanaged<
+ delegate* unmanaged,
+ delegate* unmanaged> setInvalidParameterHandler;
+
+ /// Parsed gfdata.gfd file, containing bitmap font icon lookup table.
+ private readonly GfdFile gfd;
+
+ /// Parsed , containing colors to use with
+ /// .
+ private readonly uint[] colorTypes;
+
+ /// Parsed , containing colors to use with
+ /// .
+ private readonly uint[] edgeColorTypes;
+
+ /// Parsed text fragments from a SeString.
+ /// Touched only from the main thread.
+ private readonly List fragments = [];
+
+ /// Foreground color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List colorStack = [];
+
+ /// Edge/border color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List edgeColorStack = [];
+
+ /// Shadow color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List shadowColorStack = [];
+
+ /// Splits a draw list so that different layers of a single glyph can be drawn out of order.
+ private ImDrawListSplitter* splitter = ImGuiNative.ImDrawListSplitter_ImDrawListSplitter();
+
+ [ServiceManager.ServiceConstructor]
+ private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
+ {
+ 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)
+ {
+ // Contains ABGR.
+ this.colorTypes[row.RowId] = row.UIForeground;
+ this.edgeColorTypes[row.RowId] = row.UIGlow;
+ }
+
+ if (BitConverter.IsLittleEndian)
+ {
+ // ImGui wants RGBA in LE.
+ foreach (ref var r in this.colorTypes.AsSpan())
+ r = BinaryPrimitives.ReverseEndianness(r);
+ foreach (ref var r in this.edgeColorTypes.AsSpan())
+ r = BinaryPrimitives.ReverseEndianness(r);
+ }
+
+ this.gfd = dm.GetFile("common/font/gfdata.gfd")!;
+
+ // SetUnhandledExceptionFilter(who cares);
+ // _set_purecall_handler(() => *(int*)0 = 0xff14);
+ // _set_invalid_parameter_handler(() => *(int*)0 = 0xff14);
+ var f = sigScanner.ScanText(
+ "ff 15 ff 0e e3 01 48 8d 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 48 8d 0d ?? ?? ?? ?? e8 ?? ?? ?? ??") + 26;
+ fixed (void* p = &this.setInvalidParameterHandler)
+ *(nint*)p = *(int*)f + f + 4;
+ }
+
+ /// Finalizes an instance of the class.
+ ~SeStringRenderer() => this.ReleaseUnmanagedResources();
+
+ ///
+ void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
+
+ /// Compiles a SeString from a text macro representation.
+ /// SeString text macro representation.
+ /// Compiled SeString.
+ public ReadOnlySeString Compile(ReadOnlySpan text)
+ {
+ // MacroEncoder looks stateful; disallowing calls from off main threads for now.
+ ThreadSafety.AssertMainThread();
+
+ var prev = this.setInvalidParameterHandler(&MsvcrtInvalidParameterHandlerDetour);
+ try
+ {
+ using var tmp = new Utf8String();
+ RaptureTextModule.Instance()->MacroEncoder.EncodeString(&tmp, text);
+ return new(tmp.AsSpan().ToArray());
+ }
+ catch (Exception)
+ {
+ return MacroEncoderEncodeStringError;
+ }
+ finally
+ {
+ this.setInvalidParameterHandler(prev);
+ }
+
+ [UnmanagedCallersOnly]
+ static void MsvcrtInvalidParameterHandlerDetour(char* a, char* b, char* c, int d, nuint e) =>
+ throw new InvalidOperationException();
+ }
+
+ /// Compiles a SeString from a text macro representation.
+ /// SeString text macro representation.
+ /// Compiled SeString.
+ public ReadOnlySeString Compile(ReadOnlySpan text)
+ {
+ var len = Encoding.UTF8.GetByteCount(text);
+ if (len >= 1024)
+ {
+ var buf = ArrayPool.Shared.Rent(len + 1);
+ buf[Encoding.UTF8.GetBytes(text, buf)] = 0;
+ var res = this.Compile(buf);
+ ArrayPool.Shared.Return(buf);
+ return res;
+ }
+ else
+ {
+ Span buf = stackalloc byte[len + 1];
+ buf[Encoding.UTF8.GetBytes(text, buf)] = 0;
+ return this.Compile(buf);
+ }
+ }
+
+ /// Compiles and caches a SeString from a text macro representation.
+ /// SeString text macro representation.
+ /// Newline characters will be normalized to newline payloads.
+ /// Compiled SeString.
+ public ReadOnlySeString CompileAndCache(string text)
+ {
+ // MacroEncoder looks stateful; disallowing calls from off main threads for now.
+ // Note that this is replicated in context.Compile. Only access cache from the main thread.
+ ThreadSafety.AssertMainThread();
+
+ return this.cache.GetOrAdd(
+ text,
+ static (text, context) => context.Compile(text.ReplaceLineEndings("
")),
+ this);
+ }
+
+ /// Compiles and caches a SeString from a text macro representation, and then draws it.
+ /// SeString text macro representation.
+ /// Newline characters will be normalized to newline payloads.
+ /// Parameters for drawing.
+ /// ImGui ID, if link functionality is desired.
+ /// Button flags to use on link interaction.
+ /// Interaction result of the rendered text.
+ public SeStringDrawResult CompileAndDrawWrapped(
+ string text,
+ in SeStringDrawParams drawParams = default,
+ ImGuiId imGuiId = default,
+ ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
+ this.Draw(this.CompileAndCache(text).AsSpan(), drawParams, imGuiId, buttonFlags);
+
+ /// Draws a SeString.
+ /// SeString to draw.
+ /// Parameters for drawing.
+ /// ImGui ID, if link functionality is desired.
+ /// Button flags to use on link interaction.
+ /// Interaction result of the rendered text.
+ public SeStringDrawResult Draw(
+ in Utf8String utf8String,
+ in SeStringDrawParams drawParams = default,
+ ImGuiId imGuiId = default,
+ ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
+ this.Draw(utf8String.AsSpan(), drawParams, imGuiId, buttonFlags);
+
+ /// Draws a SeString.
+ /// SeString to draw.
+ /// Parameters for drawing.
+ /// ImGui ID, if link functionality is desired.
+ /// Button flags to use on link interaction.
+ /// Interaction result of the rendered text.
+ public SeStringDrawResult Draw(
+ ReadOnlySeStringSpan sss,
+ in SeStringDrawParams drawParams = default,
+ ImGuiId imGuiId = default,
+ ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
+ {
+ // Drawing is only valid if done from the main thread anyway, especially with interactivity.
+ ThreadSafety.AssertMainThread();
+
+ if (drawParams.TargetDrawList is not null && imGuiId)
+ throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
+
+ // This also does argument validation for drawParams. Do it here.
+ var state = new DrawState(sss, new(drawParams), this.splitter);
+
+ // Reset and initialize the state.
+ this.fragments.Clear();
+ this.colorStack.Clear();
+ this.edgeColorStack.Clear();
+ this.shadowColorStack.Clear();
+ this.colorStack.Add(state.Params.Color);
+ this.edgeColorStack.Add(state.Params.EdgeColor);
+ this.shadowColorStack.Add(state.Params.ShadowColor);
+ state.Params.Color = ApplyOpacityValue(state.Params.Color, state.Params.Opacity);
+ state.Params.EdgeColor = ApplyOpacityValue(state.Params.EdgeColor, state.Params.EdgeOpacity);
+ state.Params.ShadowColor = ApplyOpacityValue(state.Params.ShadowColor, state.Params.Opacity);
+
+ // Handle cases where ImGui.AlignTextToFramePadding has been called.
+ var pCurrentWindow = *(nint*)(ImGui.GetCurrentContext() + ImGuiContextCurrentWindowOffset);
+ var pWindowDc = pCurrentWindow + ImGuiWindowDcOffset;
+ var currLineTextBaseOffset = *(float*)(pWindowDc + ImGuiWindowTempDataCurrLineTextBaseOffset);
+
+ // Analyze the provided SeString and break it up to text fragments.
+ this.CreateTextFragments(ref state, currLineTextBaseOffset);
+ var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments);
+
+ // Calculate size.
+ var size = Vector2.Zero;
+ foreach (ref var fragment in fragmentSpan)
+ size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.Params.LineHeight));
+
+ // If we're not drawing at all, stop further processing.
+ if (state.Params.DrawList is null)
+ return new() { Size = size };
+
+ ImGuiNative.ImDrawListSplitter_Split(state.Splitter, state.Params.DrawList, ChannelCount);
+
+ // Draw all text fragments.
+ var lastRune = default(Rune);
+ foreach (ref var f in fragmentSpan)
+ {
+ var data = state.Raw.Data[f.From..f.To];
+ this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link);
+ lastRune = f.LastRune;
+ }
+
+ // Create an ImGui item, if a target draw list is not manually set.
+ if (drawParams.TargetDrawList is null)
+ ImGui.Dummy(size);
+
+ // Handle link interactions.
+ var clicked = false;
+ var hoveredLinkOffset = -1;
+ var activeLinkOffset = -1;
+ if (imGuiId.PushId())
+ {
+ var invisibleButtonDrawn = false;
+ foreach (ref readonly var f in fragmentSpan)
+ {
+ if (f.Link == -1)
+ continue;
+
+ var pos = ImGui.GetMousePos() - state.ScreenOffset - f.Offset;
+ var sz = new Vector2(f.AdvanceWidth, state.Params.LineHeight);
+ if (pos is { X: >= 0, Y: >= 0 } && pos.X <= sz.X && pos.Y <= sz.Y)
+ {
+ invisibleButtonDrawn = true;
+
+ var cursorPosBackup = ImGui.GetCursorScreenPos();
+ ImGui.SetCursorScreenPos(state.ScreenOffset + f.Offset);
+ clicked = ImGui.InvisibleButton("##link", sz, buttonFlags);
+ if (ImGui.IsItemHovered())
+ hoveredLinkOffset = f.Link;
+ if (ImGui.IsItemActive())
+ activeLinkOffset = f.Link;
+ ImGui.SetCursorScreenPos(cursorPosBackup);
+
+ break;
+ }
+ }
+
+ // If no link was hovered and thus no invisible button is put, treat the whole area as the button.
+ if (!invisibleButtonDrawn)
+ {
+ ImGui.SetCursorScreenPos(state.ScreenOffset);
+ clicked = ImGui.InvisibleButton("##text", size, buttonFlags);
+ }
+
+ ImGui.PopID();
+ }
+
+ // If any link is being interacted, draw rectangles behind the relevant text fragments.
+ if (hoveredLinkOffset != -1 || activeLinkOffset != -1)
+ {
+ state.SetCurrentChannel(ChannelLinkBackground);
+ var color = activeLinkOffset == -1 ? state.Params.LinkHoverBackColor : state.Params.LinkActiveBackColor;
+ color = ApplyOpacityValue(color, state.Params.Opacity);
+ foreach (ref readonly var fragment in fragmentSpan)
+ {
+ if (fragment.Link != hoveredLinkOffset && hoveredLinkOffset != -1)
+ continue;
+ if (fragment.Link != activeLinkOffset && activeLinkOffset != -1)
+ continue;
+ ImGuiNative.ImDrawList_AddRectFilled(
+ state.Params.DrawList,
+ state.ScreenOffset + fragment.Offset,
+ state.ScreenOffset + fragment.Offset + new Vector2(fragment.AdvanceWidth, state.Params.LineHeight),
+ color,
+ 0,
+ ImDrawFlags.None);
+ }
+ }
+
+ ImGuiNative.ImDrawListSplitter_Merge(state.Splitter, state.Params.DrawList);
+
+ var payloadEnumerator = new ReadOnlySeStringSpan(
+ hoveredLinkOffset == -1 ? ReadOnlySpan.Empty : sss.Data[hoveredLinkOffset..]).GetEnumerator();
+ if (!payloadEnumerator.MoveNext())
+ return new() { Size = size, Clicked = clicked, InteractedPayloadOffset = -1 };
+ return new()
+ {
+ Size = size,
+ Clicked = clicked,
+ InteractedPayloadOffset = hoveredLinkOffset,
+ InteractedPayloadEnvelope = sss.Data.Slice(hoveredLinkOffset, payloadEnumerator.Current.EnvelopeByteLength),
+ };
+ }
+
+ /// Gets the effective char for the given char, or null(\0) if it should not be handled at all.
+ /// Character to determine.
+ /// Corresponding rune.
+ /// Whether to display soft hyphens.
+ /// Rune corresponding to the unicode codepoint to process, or null(\0) if none.
+ private static bool TryGetDisplayRune(Rune rune, out Rune displayRune, bool displaySoftHyphen = true)
+ {
+ displayRune = rune.Value switch
+ {
+ 0 or char.MaxValue => default,
+ SoftHyphen => displaySoftHyphen ? new('-') : default,
+ _ when UnicodeData.LineBreak[rune.Value]
+ is UnicodeLineBreakClass.BK
+ or UnicodeLineBreakClass.CR
+ or UnicodeLineBreakClass.LF
+ or UnicodeLineBreakClass.NL => new(0),
+ _ => rune,
+ };
+ return displayRune.Value != 0;
+ }
+
+ /// Swaps red and blue channels of a given color in ARGB(BB GG RR AA) and ABGR(RR GG BB AA).
+ /// Color to process.
+ /// Swapped color.
+ private static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
+
+ /// Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.
+ /// RGBA value to transform.
+ /// Opacity to apply.
+ /// Transformed value.
+ private static uint ApplyOpacityValue(uint rgba, float opacity) =>
+ ((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (this.splitter is not null)
+ {
+ ImGuiNative.ImDrawListSplitter_destroy(this.splitter);
+ this.splitter = null;
+ }
+ }
+
+ /// Creates text fragment, taking line and word breaking into account.
+ /// Draw state.
+ /// Y offset adjustment for all text fragments. Used to honor
+ /// .
+ private void CreateTextFragments(ref DrawState state, float baseY)
+ {
+ var prev = 0;
+ var xy = new Vector2(0, baseY);
+ var w = 0f;
+ var linkOffset = -1;
+ foreach (var (breakAt, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString))
+ {
+ var nextLinkOffset = linkOffset;
+ for (var first = true; prev < breakAt; first = false)
+ {
+ var curr = breakAt;
+
+ // Try to split by link payloads.
+ foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..breakAt]).GetOffsetEnumerator())
+ {
+ if (p.Payload.MacroCode == MacroCode.Link)
+ {
+ nextLinkOffset =
+ p.Payload.TryGetExpression(out var e) &&
+ e.TryGetUInt(out var u) &&
+ u == (uint)LinkMacroPayloadType.Terminator
+ ? -1
+ : prev + p.Offset;
+
+ // Split only if we're not splitting at the beginning.
+ if (p.Offset != 0)
+ {
+ curr = prev + p.Offset;
+ break;
+ }
+
+ linkOffset = nextLinkOffset;
+ }
+ }
+
+ // Create a text fragment without applying wrap width limits for testing.
+ var fragment = state.CreateFragment(this, prev, curr, curr == breakAt && mandatory, xy, linkOffset);
+ var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.Params.WrapWidth;
+
+ // Test if the fragment does not fit into the current line and the current line is not empty,
+ // if this is the first time testing the current break unit.
+ if (first && xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
+ {
+ // The break unit as a whole does not fit into the current line. Advance to the next line.
+ xy.X = 0;
+ xy.Y += state.Params.LineHeight;
+ w = 0;
+ CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
+ fragment.Offset = xy;
+
+ // Now that the fragment is given its own line, test if it overflows again.
+ overflows = fragment.VisibleWidth > state.Params.WrapWidth;
+ }
+
+ if (overflows)
+ {
+ // Create a fragment again that fits into the given width limit.
+ var remainingWidth = state.Params.WrapWidth - xy.X;
+ fragment = state.CreateFragment(this, prev, curr, true, xy, linkOffset, remainingWidth);
+ }
+ else if (this.fragments.Count > 0 && xy.X != 0)
+ {
+ // New fragment fits into the current line, and it has a previous fragment in the same line.
+ // If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
+ // trailing soft hyphens are not considered.
+ if (this.fragments[^1].EndsWithSoftHyphen)
+ xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
+
+ // Adjust this fragment's offset from kerning distance.
+ xy.X += state.CalculateDistance(this.fragments[^1].LastRune, fragment.FirstRune);
+ fragment.Offset = xy;
+ }
+
+ // If the fragment was not broken by wrap width, update the link payload offset.
+ if (fragment.To == curr)
+ linkOffset = nextLinkOffset;
+
+ w = Math.Max(w, xy.X + fragment.VisibleWidth);
+ xy.X += fragment.AdvanceWidth;
+ prev = fragment.To;
+ this.fragments.Add(fragment);
+
+ if (fragment.BreakAfter)
+ {
+ xy.X = w = 0;
+ xy.Y += state.Params.LineHeight;
+ }
+ }
+ }
+ }
+
+ /// Draws a text fragment.
+ /// Draw state.
+ /// Offset of left top corner of this text fragment in pixels w.r.t.
+ /// .
+ /// Whether to display soft hyphens in this text fragment.
+ /// Byte span of the SeString fragment to draw.
+ /// Rune that preceded this text fragment in the same line, or 0 if none.
+ /// Byte offset of the link payload that decorates this text fragment in
+ /// , or -1 if none.
+ private void DrawTextFragment(
+ ref DrawState state,
+ Vector2 offset,
+ bool displaySoftHyphen,
+ ReadOnlySpan span,
+ Rune lastRune,
+ int link)
+ {
+ 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 is { IsSeStringPayload: true, EffectiveInt: char.MaxValue or ObjectReplacementCharacter })
+ {
+ var enu = new ReadOnlySeStringSpan(span[c.ByteOffset..]).GetOffsetEnumerator();
+ if (!enu.MoveNext())
+ continue;
+
+ var payload = enu.Current.Payload;
+ switch (payload.MacroCode)
+ {
+ case MacroCode.Color:
+ state.Params.Color = ApplyOpacityValue(
+ TouchColorStack(this.colorStack, payload),
+ state.Params.Opacity);
+ continue;
+ case MacroCode.EdgeColor:
+ state.Params.EdgeColor = TouchColorStack(this.edgeColorStack, payload);
+ state.Params.EdgeColor = ApplyOpacityValue(
+ state.Params.ForceEdgeColor ? this.edgeColorStack[0] : state.Params.EdgeColor,
+ state.Params.EdgeOpacity);
+ continue;
+ case MacroCode.ShadowColor:
+ state.Params.ShadowColor = ApplyOpacityValue(
+ TouchColorStack(this.shadowColorStack, payload),
+ state.Params.Opacity);
+ continue;
+ case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ // doesn't actually work in chat log
+ state.Params.Bold = u != 0;
+ continue;
+ case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ state.Params.Italic = u != 0;
+ continue;
+ case MacroCode.Edge when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ state.Params.Edge = u != 0;
+ continue;
+ case MacroCode.Shadow when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ state.Params.Shadow = u != 0;
+ continue;
+ case MacroCode.ColorType:
+ state.Params.Color = ApplyOpacityValue(
+ TouchColorTypeStack(this.colorStack, this.colorTypes, payload),
+ state.Params.Opacity);
+ continue;
+ case MacroCode.EdgeColorType:
+ state.Params.EdgeColor = TouchColorTypeStack(this.edgeColorStack, this.edgeColorTypes, payload);
+ state.Params.EdgeColor = ApplyOpacityValue(
+ state.Params.ForceEdgeColor ? this.edgeColorStack[0] : state.Params.EdgeColor,
+ state.Params.EdgeOpacity);
+ 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 size = state.CalculateGfdEntrySize(gfdEntry, out var useHq);
+ state.SetCurrentChannel(ChannelFore);
+ state.Draw(
+ offset + new Vector2(x, MathF.Round((state.Params.LineHeight - size.Y) / 2)),
+ gfdTextureSrv,
+ Vector2.Zero,
+ size,
+ Vector2.Zero,
+ useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
+ useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
+ ApplyOpacityValue(uint.MaxValue, state.Params.Opacity));
+ if (link != -1)
+ state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X);
+
+ width = Math.Max(width, x + size.X);
+ x += MathF.Round(size.X);
+ lastRune = default;
+ continue;
+ }
+
+ default:
+ continue;
+ }
+ }
+
+ if (!TryGetDisplayRune(c.EffectiveRune, out var rune, displaySoftHyphen))
+ continue;
+
+ ref var g = ref state.FindGlyph(ref rune);
+ var dist = state.CalculateDistance(lastRune, rune);
+ lastRune = rune;
+
+ var dxBold = state.Params.Bold ? 2 : 1;
+ var dyItalic = state.Params.Italic
+ ? new Vector2(state.Params.FontSize - g.Y0, state.Params.FontSize - g.Y1) / 6
+ : Vector2.Zero;
+
+ if (state.Params is { Shadow: true, ShadowColor: >= 0x1000000 })
+ {
+ state.SetCurrentChannel(ChannelShadow);
+ for (var dx = 0; dx < dxBold; dx++)
+ state.Draw(offset + new Vector2(x + dist + dx, 1), g, dyItalic, state.Params.ShadowColor);
+ }
+
+ if ((state.Params.Edge || this.edgeColorStack.Count > 1) && state.Params.EdgeColor >= 0x1000000)
+ {
+ state.SetCurrentChannel(ChannelEdge);
+ for (var dx = -1; dx <= dxBold; dx++)
+ {
+ for (var dy = -1; dy <= 1; dy++)
+ {
+ if (dx >= 0 && dx < dxBold && dy == 0)
+ continue;
+
+ state.Draw(offset + new Vector2(x + dist + dx, dy), g, dyItalic, state.Params.EdgeColor);
+ }
+ }
+ }
+
+ state.SetCurrentChannel(ChannelFore);
+ for (var dx = 0; dx < dxBold; dx++)
+ state.Draw(offset + new Vector2(x + dist + dx, 0), g, dyItalic, state.Params.Color);
+
+ if (link != -1)
+ state.DrawLinkUnderline(offset + new Vector2(x + dist, 0), g.AdvanceX);
+
+ width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale));
+ x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
+ }
+
+ return;
+
+ static uint TouchColorStack(List rgbaStack, ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var expr))
+ return rgbaStack[^1];
+
+ // Color payloads have BGRA values as its parameter. ImGui expects RGBA values.
+ // Opacity component is ignored.
+ if (expr.TryGetPlaceholderExpression(out var p) && p == (int)ExpressionType.StackColor)
+ {
+ // First item in the stack is the color we started to draw with.
+ if (rgbaStack.Count > 1)
+ rgbaStack.RemoveAt(rgbaStack.Count - 1);
+ return rgbaStack[^1];
+ }
+
+ if (expr.TryGetUInt(out var bgra))
+ {
+ rgbaStack.Add(SwapRedBlue(bgra) | 0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ if (expr.TryGetParameterExpression(out var et, out var op) &&
+ et == (int)ExpressionType.GlobalNumber &&
+ op.TryGetInt(out var i) &&
+ RaptureTextModule.Instance() is var rtm &&
+ rtm is not null &&
+ i > 0 && i <= rtm->TextModule.MacroDecoder.GlobalParameters.Count &&
+ rtm->TextModule.MacroDecoder.GlobalParameters[i - 1] is { Type: TextParameterType.Integer } gp)
+ {
+ rgbaStack.Add(SwapRedBlue((uint)gp.IntValue) | 0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ // Fallback value.
+ rgbaStack.Add(0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ static uint TouchColorTypeStack(List rgbaStack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var expr))
+ return rgbaStack[^1];
+ if (!expr.TryGetUInt(out var colorTypeIndex))
+ return rgbaStack[^1];
+
+ if (colorTypeIndex == 0)
+ {
+ // First item in the stack is the color we started to draw with.
+ if (rgbaStack.Count > 1)
+ rgbaStack.RemoveAt(rgbaStack.Count - 1);
+ return rgbaStack[^1];
+ }
+
+ // Opacity component is ignored.
+ rgbaStack.Add((colorTypeIndex < colorTypes.Length ? colorTypes[colorTypeIndex] : 0u) | 0xFF000000u);
+
+ return rgbaStack[^1];
+ }
+ }
+
+ /// Determines a bitmap icon to display for the given SeString payload.
+ /// Byte span that should include a SeString payload.
+ /// Icon to display, or if it should not be displayed as an icon.
+ 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)
+ {
+ // Show the specified icon as-is.
+ case MacroCode.Icon
+ when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
+ return (BitmapFontIcon)iconId;
+
+ // Apply gamepad key mapping to icons.
+ 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 => ControllerButton0,
+ PadButtonValue.Cancel => ControllerButton1,
+ 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;
+ }
+
+ /// Represents a text fragment in a SeString span.
+ /// Starting byte offset (inclusive) in a SeString.
+ /// Ending byte offset (exclusive) in a SeString.
+ /// Byte offset of the link that decorates this text fragment, or -1 if none.
+ /// Offset in pixels w.r.t. .
+ /// Visible width of this text fragment. This is the width required to draw everything
+ /// without clipping.
+ /// Advance width of this text fragment. This is the width required to add to the cursor
+ /// to position the next fragment correctly.
+ /// Same with , but trimming all the
+ /// trailing soft hyphens.
+ /// Whether to insert a line break after this text fragment.
+ /// Whether this text fragment ends with one or more soft hyphens.
+ /// First rune in this text fragment.
+ /// Last rune in this text fragment, for the purpose of calculating kerning distance with
+ /// the following text fragment in the same line, if any.
+ private record struct TextFragment(
+ int From,
+ int To,
+ int Link,
+ Vector2 Offset,
+ float VisibleWidth,
+ float AdvanceWidth,
+ float AdvanceWidthWithoutSoftHyphen,
+ bool BreakAfter,
+ bool EndsWithSoftHyphen,
+ Rune FirstRune,
+ Rune LastRune)
+ {
+ public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
+ }
+
+ /// Represents a temporary state required for drawing.
+ private ref struct DrawState(
+ ReadOnlySeStringSpan raw,
+ SeStringDrawParams.Resolved @params,
+ ImDrawListSplitter* splitter)
+ {
+ /// Raw SeString span.
+ public readonly ReadOnlySeStringSpan Raw = raw;
+
+ /// Multiplier value for glyph metrics, so that it scales to .
+ ///
+ public readonly float FontSizeScale = @params.FontSize / @params.Font->FontSize;
+
+ /// Value obtained from .
+ public readonly Vector2 ScreenOffset = @params.ScreenOffset;
+
+ /// Splitter to split .
+ public readonly ImDrawListSplitter* Splitter = splitter;
+
+ /// Resolved draw parameters from the caller.
+ public SeStringDrawParams.Resolved Params = @params;
+
+ /// Calculates the size in pixels of a GFD entry when drawn.
+ /// GFD entry to determine the size.
+ /// Whether to draw the HQ texture.
+ /// Determined size of the GFD entry when drawn.
+ public readonly Vector2 CalculateGfdEntrySize(in GfdFile.GfdEntry gfdEntry, out bool useHq)
+ {
+ useHq = this.Params.FontSize > 20;
+ var targetHeight = useHq ? this.Params.FontSize : 20;
+ return new(gfdEntry.Width * (targetHeight / gfdEntry.Height), targetHeight);
+ }
+
+ /// Sets the current channel in the ImGui draw list splitter.
+ /// Channel to switch to.
+ public readonly void SetCurrentChannel(int channelIndex) =>
+ ImGuiNative.ImDrawListSplitter_SetCurrentChannel(
+ this.Splitter,
+ this.Params.DrawList,
+ channelIndex);
+
+ /// Draws a single glyph.
+ /// Offset of the glyph in pixels w.r.t.
+ /// .
+ /// Glyph to draw.
+ /// Transformation for that will push top and bottom pixels to
+ /// apply faux italicization.
+ /// Color of the glyph.
+ public readonly void Draw(Vector2 offset, in ImGuiHelpers.ImFontGlyphReal g, Vector2 dyItalic, uint color) =>
+ this.Draw(
+ offset + new Vector2(
+ 0,
+ MathF.Round(((this.Params.LineHeight - this.Params.Font->FontSize) * this.FontSizeScale) / 2f)),
+ this.Params.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID,
+ g.XY0 * this.FontSizeScale,
+ g.XY1 * this.FontSizeScale,
+ dyItalic * this.FontSizeScale,
+ g.UV0,
+ g.UV1,
+ color);
+
+ /// Draws a single glyph.
+ /// Offset of the glyph in pixels w.r.t.
+ /// .
+ /// ImGui texture ID to draw from.
+ /// Left top corner of the glyph w.r.t. its glyph origin in the target draw list.
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.
+ /// Transformation for and that will push
+ /// top and bottom pixels to apply faux italicization.
+ /// Left top corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Color of the glyph.
+ public readonly void Draw(
+ Vector2 offset,
+ nint igTextureId,
+ Vector2 xy0,
+ Vector2 xy1,
+ Vector2 dyItalic,
+ Vector2 uv0,
+ Vector2 uv1,
+ uint color = uint.MaxValue)
+ {
+ offset += this.ScreenOffset;
+ ImGuiNative.ImDrawList_AddImageQuad(
+ this.Params.DrawList,
+ 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);
+ }
+
+ /// Draws an underline, for links.
+ /// Offset of the glyph in pixels w.r.t.
+ /// .
+ /// Advance width of the glyph.
+ public readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
+ {
+ if (this.Params.LinkUnderlineThickness < 1f)
+ return;
+
+ var dy = (this.Params.LinkUnderlineThickness - 1) / 2f;
+ dy += MathF.Round(
+ (((this.Params.LineHeight - this.Params.FontSize) / 2) + this.Params.Font->Ascent) *
+ this.FontSizeScale);
+ this.SetCurrentChannel(ChannelLinkUnderline);
+ ImGuiNative.ImDrawList_AddLine(
+ this.Params.DrawList,
+ this.ScreenOffset + offset + new Vector2(0, dy),
+ this.ScreenOffset + offset + new Vector2(advanceWidth, dy),
+ this.Params.Color,
+ this.Params.LinkUnderlineThickness);
+
+ if (this.Params is { Shadow: true, ShadowColor: >= 0x1000000 })
+ {
+ this.SetCurrentChannel(ChannelShadow);
+ ImGuiNative.ImDrawList_AddLine(
+ this.Params.DrawList,
+ this.ScreenOffset + offset + new Vector2(0, dy + 1),
+ this.ScreenOffset + offset + new Vector2(advanceWidth, dy + 1),
+ this.Params.ShadowColor,
+ this.Params.LinkUnderlineThickness);
+ }
+ }
+
+ /// Creates a text fragment.
+ /// Associated renderer.
+ /// Starting byte offset (inclusive) in that this fragment deals with.
+ ///
+ /// Ending byte offset (exclusive) in that this fragment deals with.
+ /// Whether to break line after this fragment.
+ /// Offset in pixels w.r.t. .
+ /// Byte offset of the link payload in that decorates this
+ /// text fragment.
+ /// Optional wrap width to stop at while creating this text fragment. Note that at least
+ /// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
+ /// the wrap width.
+ /// Newly created text fragment.
+ public readonly TextFragment CreateFragment(
+ SeStringRenderer renderer,
+ int from,
+ int to,
+ bool breakAfter,
+ Vector2 offset,
+ int activeLinkOffset,
+ float wrapWidth = float.MaxValue)
+ {
+ var x = 0f;
+ var w = 0f;
+ var visibleWidth = 0f;
+ var advanceWidth = 0f;
+ var advanceWidthWithoutSoftHyphen = 0f;
+ var firstDisplayRune = default(Rune?);
+ var lastDisplayRune = default(Rune);
+ var lastNonSoftHyphenRune = default(Rune);
+ var endsWithSoftHyphen = false;
+ foreach (var c in UtfEnumerator.From(this.Raw.Data[from..to], UtfEnumeratorFlags.Utf8SeString))
+ {
+ var byteOffset = from + c.ByteOffset;
+ var isBreakableWhitespace = false;
+ var effectiveRune = c.EffectiveRune;
+ Rune displayRune;
+ 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)
+ {
+ // This is an icon payload.
+ var size = this.CalculateGfdEntrySize(gfdEntry, out _);
+ w = Math.Max(w, x + size.X);
+ x += MathF.Round(size.X);
+ displayRune = default;
+ }
+ else if (TryGetDisplayRune(effectiveRune, out displayRune))
+ {
+ // This is a printable character, or a standard whitespace character.
+ ref var g = ref this.FindGlyph(ref displayRune);
+ var dist = this.CalculateDistance(lastDisplayRune, displayRune);
+ w = Math.Max(w, x + ((dist + g.X1) * this.FontSizeScale));
+ x += MathF.Round((dist + g.AdvanceX) * this.FontSizeScale);
+
+ isBreakableWhitespace =
+ Rune.IsWhiteSpace(displayRune) &&
+ UnicodeData.LineBreak[displayRune.Value] is not UnicodeLineBreakClass.GL;
+ }
+ else
+ {
+ continue;
+ }
+
+ if (isBreakableWhitespace)
+ {
+ advanceWidth = x;
+ }
+ else
+ {
+ if (firstDisplayRune is not null && w > wrapWidth && effectiveRune.Value != SoftHyphen)
+ {
+ to = byteOffset;
+ break;
+ }
+
+ advanceWidth = x;
+ visibleWidth = w;
+ }
+
+ firstDisplayRune ??= displayRune;
+ lastDisplayRune = displayRune;
+ endsWithSoftHyphen = effectiveRune.Value == SoftHyphen;
+ if (!endsWithSoftHyphen)
+ {
+ advanceWidthWithoutSoftHyphen = x;
+ lastNonSoftHyphenRune = displayRune;
+ }
+ }
+
+ return new(
+ from,
+ to,
+ activeLinkOffset,
+ offset,
+ visibleWidth,
+ advanceWidth,
+ advanceWidthWithoutSoftHyphen,
+ breakAfter,
+ endsWithSoftHyphen,
+ firstDisplayRune ?? default,
+ lastNonSoftHyphenRune);
+ }
+
+ /// Gets the glyph corresponding to the given codepoint.
+ /// An instance of that represents a character to display.
+ /// Corresponding glyph, or glyph of a fallback character specified from
+ /// .
+ public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
+ {
+ var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
+ ? ImGuiNative.ImFont_FindGlyph(this.Params.Font, (ushort)rune.Value)
+ : this.Params.Font->FallbackGlyph;
+ return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
+ }
+
+ /// Gets the glyph corresponding to the given codepoint.
+ /// An instance of that represents a character to display, that will be
+ /// changed on return to the rune corresponding to the fallback glyph if a glyph not corresponding to the
+ /// requested glyph is being returned.
+ /// Corresponding glyph, or glyph of a fallback character specified from
+ /// .
+ public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(ref Rune rune)
+ {
+ ref var glyph = ref this.FindGlyph(rune);
+ if (rune.Value != glyph.Codepoint && !Rune.TryCreate(glyph.Codepoint, out rune))
+ rune = Rune.ReplacementChar;
+ return ref glyph;
+ }
+
+ /// Gets the kerning adjustment between two glyphs in a succession corresponding to the given runes.
+ ///
+ /// Rune representing the glyph on the left side of a pair.
+ /// Rune representing the glyph on the right side of a pair.
+ /// Distance adjustment in pixels, scaled to the size specified from
+ /// , and rounded.
+ public readonly float CalculateDistance(Rune left, Rune right)
+ {
+ // Kerning distance entries are ignored if NUL, U+FFFF(invalid Unicode character), or characters outside
+ // the basic multilingual plane(BMP) is involved.
+ if (left.Value is <= 0 or >= char.MaxValue)
+ return 0;
+ if (right.Value is <= 0 or >= char.MaxValue)
+ return 0;
+
+ return MathF.Round(
+ ImGuiNative.ImFont_GetDistanceAdjustmentForPair(
+ this.Params.Font,
+ (ushort)left.Value,
+ (ushort)right.Value) * this.FontSizeScale);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/DerivedGeneralCategory.txt b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/DerivedGeneralCategory.txt
similarity index 100%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/DerivedGeneralCategory.txt
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/DerivedGeneralCategory.txt
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/EastAsianWidth.txt b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/EastAsianWidth.txt
similarity index 100%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/EastAsianWidth.txt
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/EastAsianWidth.txt
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/LineBreak.txt b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreak.txt
similarity index 100%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/LineBreak.txt
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreak.txt
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/LineBreakEnumerator.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
similarity index 98%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/LineBreakEnumerator.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
index 49e2298b0..9113ef703 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/LineBreakEnumerator.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
@@ -2,11 +2,11 @@ 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;
+using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeEastAsianWidthClass;
+using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeGeneralCategory;
+using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeLineBreakClass;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Enumerates line break offsets.
internal ref struct LineBreakEnumerator
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeData.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeData.cs
similarity index 98%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeData.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeData.cs
index ffbd92cc5..3e4f74ada 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeData.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeData.cs
@@ -3,7 +3,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Stores unicode data.
internal static class UnicodeData
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEastAsianWidthClass.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEastAsianWidthClass.cs
similarity index 90%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEastAsianWidthClass.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEastAsianWidthClass.cs
index 0335b29a0..184168795 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEastAsianWidthClass.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEastAsianWidthClass.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Unicode east asian width.
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEmojiProperty.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEmojiProperty.cs
similarity index 94%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEmojiProperty.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEmojiProperty.cs
index 3788d9d99..3952a5178 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeEmojiProperty.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeEmojiProperty.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Unicode emoji property.
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeGeneralCategory.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeGeneralCategory.cs
similarity index 97%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeGeneralCategory.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeGeneralCategory.cs
index 007666031..f24f5b357 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeGeneralCategory.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeGeneralCategory.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Unicode general category..
///
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeLineBreakClass.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeLineBreakClass.cs
similarity index 98%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeLineBreakClass.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeLineBreakClass.cs
index 0ee5a50a3..bbab3170f 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UnicodeLineBreakClass.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UnicodeLineBreakClass.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Unicode line break class.
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumerator.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumerator.cs
similarity index 95%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumerator.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumerator.cs
index 6d319ed92..b73bc85e4 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumerator.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumerator.cs
@@ -8,7 +8,7 @@ using Lumina.Text;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Enumerates a UTF-N byte sequence by codepoint.
[DebuggerDisplay("{Current}/{data.Length} ({flags}, BE={isBigEndian})")]
@@ -257,7 +257,11 @@ internal ref struct UtfEnumerator
/// Gets the effective char value, with invalid or non-representable codepoints replaced.
///
/// if the character should not be displayed at all.
- public char EffectiveChar => this.EffectiveInt is var i and >= 0 and < char.MaxValue ? (char)i : char.MaxValue;
+ public char EffectiveChar
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.EffectiveInt is var i and >= 0 and < char.MaxValue ? (char)i : char.MaxValue;
+ }
/// Gets the effective int value, with invalid codepoints replaced.
/// if the character should not be displayed at all.
@@ -268,6 +272,14 @@ internal ref struct UtfEnumerator
? 0xFFFD
: rune.Value;
+ /// Gets the effective value, with invalid codepoints replaced.
+ /// if the character should not be displayed at all.
+ public Rune EffectiveRune
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(this.EffectiveInt);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Subsequence left, Subsequence right) => left.Equals(right);
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumeratorFlags.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumeratorFlags.cs
similarity index 96%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumeratorFlags.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumeratorFlags.cs
index 7d07049da..01380e40c 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfEnumeratorFlags.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfEnumeratorFlags.cs
@@ -1,4 +1,4 @@
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Flags on enumerating a unicode sequence.
[Flags]
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfValue.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfValue.cs
similarity index 99%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfValue.cs
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfValue.cs
index c35b117a2..6930e6ba4 100644
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/UtfValue.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/UtfValue.cs
@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
-namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
/// Represents a single value to be used in a UTF-N byte sequence.
[StructLayout(LayoutKind.Explicit, Size = 4)]
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/emoji-data.txt b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/emoji-data.txt
similarity index 100%
rename from Dalamud/Interface/Internal/ImGuiSeStringRenderer/TextProcessing/emoji-data.txt
rename to Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/emoji-data.txt
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
new file mode 100644
index 000000000..543f4c07a
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -0,0 +1,168 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+using ImGuiNET;
+
+using Lumina.Text.Payloads;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer;
+
+/// Render styles for a SeString.
+public record struct SeStringDrawParams
+{
+ /// Gets or sets the target draw list.
+ /// Target draw list, default(ImDrawListPtr) to not draw, or null to use
+ /// (the default).
+ /// If this value is set, will not be called, and ImGui ID will be ignored.
+ ///
+ public ImDrawListPtr? TargetDrawList { get; set; }
+
+ /// Gets or sets the font to use.
+ /// Font to use, or null to use (the default).
+ public ImFontPtr? Font { get; set; }
+
+ /// Gets or sets the screen offset of the left top corner.
+ /// Screen offset to draw at, or null to use .
+ public Vector2? ScreenOffset { get; set; }
+
+ /// Gets or sets the font size.
+ /// Font size in pixels, or 0 to use the current ImGui font size .
+ ///
+ public float? FontSize { get; set; }
+
+ /// Gets or sets the line height ratio.
+ /// 1 or null (the default) will use as the line height.
+ /// 2 will make line height twice the .
+ public float? LineHeight { get; set; }
+
+ /// Gets or sets the wrapping width.
+ /// Width in pixels, or null to wrap at the end of available content region from
+ /// (the default).
+ public float? WrapWidth { get; set; }
+
+ /// Gets or sets the thickness of underline under links.
+ public float? LinkUnderlineThickness { get; set; }
+
+ /// Gets or sets the opacity, commonly called "alpha".
+ /// Opacity value ranging from 0(invisible) to 1(fully visible), or null to use the current ImGui
+ /// opacity from accessed using .
+ public float? Opacity { get; set; }
+
+ /// Gets or sets the strength of the edge, which will have effects on the edge opacity.
+ /// Strength value ranging from 0(invisible) to 1(fully visible), or null to use the default value
+ /// of 0.25f that might be subject to change in the future.
+ public float? EdgeStrength { get; set; }
+
+ /// Gets or sets the color of the rendered text.
+ /// Color in RGBA, or null to use (the default).
+ public uint? Color { get; set; }
+
+ /// Gets or sets the color of the rendered text edge.
+ /// Color in RGBA, or null to use opaque black (the default).
+ public uint? EdgeColor { get; set; }
+
+ /// Gets or sets the color of the rendered text shadow.
+ /// Color in RGBA, or null to use opaque black (the default).
+ public uint? ShadowColor { get; set; }
+
+ /// Gets or sets the background color of a link when hovered.
+ /// Color in RGBA, or null to use (the default).
+ public uint? LinkHoverBackColor { get; set; }
+
+ /// Gets or sets the background color of a link when active.
+ /// Color in RGBA, or null to use (the default).
+ public uint? LinkActiveBackColor { get; set; }
+
+ /// Gets or sets a value indicating whether to force the color of the rendered text edge.
+ /// If set, then and will be
+ /// ignored.
+ public bool ForceEdgeColor { get; set; }
+
+ /// Gets or sets a value indicating whether the text is rendered bold.
+ public bool Bold { get; set; }
+
+ /// Gets or sets a value indicating whether the text is rendered italic.
+ public bool Italic { get; set; }
+
+ /// Gets or sets a value indicating whether the text is rendered with edge.
+ public bool Edge { get; set; }
+
+ /// Gets or sets a value indicating whether the text is rendered with shadow.
+ public bool Shadow { get; set; }
+
+ private readonly unsafe ImFont* EffectiveFont =>
+ (this.Font ?? ImGui.GetFont()) is var f && f.NativePtr is not null
+ ? f.NativePtr
+ : throw new ArgumentException("Specified font is empty.");
+
+ private readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
+
+ private readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
+
+ /// Calculated values from using ImGui styles.
+ [SuppressMessage(
+ "StyleCop.CSharp.OrderingRules",
+ "SA1214:Readonly fields should appear before non-readonly fields",
+ Justification = "Matching the above order.")]
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct Resolved(in SeStringDrawParams ssdp)
+ {
+ ///
+ public readonly ImDrawList* DrawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
+
+ ///
+ public readonly ImFont* Font = ssdp.EffectiveFont;
+
+ ///
+ public readonly Vector2 ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
+
+ ///
+ public readonly float FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
+
+ ///
+ public readonly float LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
+
+ ///
+ public readonly float WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
+
+ ///
+ public readonly float LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
+
+ ///
+ public readonly float Opacity = ssdp.EffectiveOpacity;
+
+ ///
+ public readonly float EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
+
+ ///
+ public uint Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
+
+ ///
+ public uint EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
+
+ ///
+ public uint ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
+
+ ///
+ public readonly uint LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
+
+ ///
+ public readonly uint LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
+
+ ///
+ public readonly bool ForceEdgeColor = ssdp.ForceEdgeColor;
+
+ ///
+ public bool Bold = ssdp.Bold;
+
+ ///
+ public bool Italic = ssdp.Italic;
+
+ ///
+ public bool Edge = ssdp.Edge;
+
+ ///
+ public bool Shadow = ssdp.Shadow;
+ }
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs
new file mode 100644
index 000000000..905e8ed23
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer;
+
+/// Represents the result of n rendered interactable SeString.
+public ref struct SeStringDrawResult
+{
+ private Payload? lazyPayload;
+
+ /// Gets the visible size of the text rendered/to be rendered.
+ public Vector2 Size { get; init; }
+
+ /// Gets a value indicating whether a payload or the whole text has been clicked.
+ public bool Clicked { get; init; }
+
+ /// Gets the offset of the interacted payload, or -1 if none.
+ public int InteractedPayloadOffset { get; init; }
+
+ /// Gets the interacted payload envelope, or if none.
+ public ReadOnlySpan InteractedPayloadEnvelope { get; init; }
+
+ /// Gets the interacted payload, or null if none.
+ public Payload? InteractedPayload =>
+ this.lazyPayload ??=
+ this.InteractedPayloadEnvelope.IsEmpty
+ ? default
+ : SeString.Parse(this.InteractedPayloadEnvelope).Payloads.FirstOrDefault();
+}
diff --git a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs b/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs
deleted file mode 100644
index face85cfc..000000000
--- a/Dalamud/Interface/Internal/ImGuiSeStringRenderer/SeStringRenderer.cs
+++ /dev/null
@@ -1,693 +0,0 @@
-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/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs
index 5db6d393a..f1a025d93 100644
--- a/Dalamud/Interface/Internal/UiDebug.cs
+++ b/Dalamud/Interface/Internal/UiDebug.cs
@@ -1,12 +1,17 @@
using System.Numerics;
using Dalamud.Game.Gui;
-using Dalamud.Interface.Internal.ImGuiSeStringRenderer;
+using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
+
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
+using Lumina.Text.ReadOnly;
+
// Customised version of https://github.com/aers/FFXIVUIDebug
namespace Dalamud.Interface.Internal;
@@ -204,10 +209,22 @@ internal unsafe class UiDebug
var textNode = (AtkTextNode*)node;
ImGui.Text("text: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textNode->NodeText);
+ Service.Get().Draw(textNode->NodeText);
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
+ ImGui.SameLine();
+ if (ImGui.Button($"Encode##{(ulong)textNode:X}"))
+ {
+ using var tmp = new Utf8String();
+ RaptureTextModule.Instance()->MacroEncoder.EncodeString(&tmp, textNode->NodeText.StringPtr);
+ textNode->NodeText.Copy(&tmp);
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button($"Decode##{(ulong)textNode:X}"))
+ textNode->NodeText.SetString(new ReadOnlySeStringSpan(textNode->NodeText.StringPtr).ToString());
+
ImGui.Text($"AlignmentType: {(AlignmentType)textNode->AlignmentFontType} FontSize: {textNode->FontSize}");
int b = textNode->AlignmentFontType;
if (ImGui.InputInt($"###setAlignment{(ulong)textNode:X}", ref b, 1))
@@ -233,7 +250,7 @@ internal unsafe class UiDebug
var counterNode = (AtkCounterNode*)node;
ImGui.Text("text: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(counterNode->NodeText);
+ Service.Get().Draw(counterNode->NodeText);
break;
case NodeType.Image:
var imageNode = (AtkImageNode*)node;
@@ -372,31 +389,31 @@ internal unsafe class UiDebug
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
ImGui.Text("InputBase Text1: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText1);
+ Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText1);
ImGui.Text("InputBase Text2: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText2);
+ Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText2);
ImGui.Text("Text1: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->UnkText01);
+ Service.Get().Draw(textInputComponent->UnkText01);
ImGui.Text("Text2: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->UnkText02);
+ Service.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("Text3: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->UnkText03);
+ Service.Get().Draw(textInputComponent->UnkText03);
ImGui.Text("Text4: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->UnkText04);
+ Service.Get().Draw(textInputComponent->UnkText04);
ImGui.Text("Text5: ");
ImGui.SameLine();
- Service.Get().DrawWrapped(textInputComponent->UnkText05);
+ Service.Get().Draw(textInputComponent->UnkText05);
break;
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index e2182aba4..6e48e2d08 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -1,9 +1,21 @@
-using System.Text;
+using System.Linq;
+using System.Text;
+using Dalamud.Data;
+using Dalamud.Game.Gui;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Interface.ImGuiSeStringRenderer;
+using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Utility;
using ImGuiNET;
+using Lumina.Excel.GeneratedSheets2;
+using Lumina.Text;
+using Lumina.Text.Payloads;
+using Lumina.Text.ReadOnly;
+
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
@@ -13,6 +25,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
{
private ImVectorWrapper testStringBuffer;
private string testString = string.Empty;
+ private Addon[]? addons;
+ private ReadOnlySeString? uicolor;
+ private ReadOnlySeString? logkind;
+ private SeStringDrawParams style;
+ private bool interactable;
///
public string DisplayName { get; init; } = "SeStringRenderer Test";
@@ -24,20 +41,234 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
public bool Ready { get; set; }
///
- public void Load() => this.Ready = true;
+ public void Load()
+ {
+ this.style = default;
+ this.addons = null;
+ this.uicolor = null;
+ this.logkind = null;
+ this.testString = string.Empty;
+ this.interactable = true;
+ this.Ready = true;
+ }
///
public void Draw()
{
+ var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text));
+ if (ImGui.ColorEdit4("Color", ref t2))
+ this.style.Color = ImGui.ColorConvertFloat4ToU32(t2);
+
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u);
+ if (ImGui.ColorEdit4("Edge Color", ref t2))
+ this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2);
+
+ ImGui.SameLine();
+ var t = this.style.ForceEdgeColor;
+ if (ImGui.Checkbox("Forced", ref t))
+ this.style.ForceEdgeColor = t;
+
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u);
+ if (ImGui.ColorEdit4("Shadow Color", ref t2))
+ this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2);
+
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered));
+ if (ImGui.ColorEdit4("Link Hover Color", ref t2))
+ this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2);
+
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive));
+ if (ImGui.ColorEdit4("Link Active Color", ref t2))
+ this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2);
+
+ var t3 = this.style.LineHeight ?? 1f;
+ if (ImGui.DragFloat("Line Height", ref t3, 0.01f, 0.4f, 3f, "%.02f"))
+ this.style.LineHeight = t3;
+
+ t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha;
+ if (ImGui.DragFloat("Opacity", ref t3, 0.005f, 0f, 1f, "%.02f"))
+ this.style.Opacity = t3;
+
+ t3 = this.style.EdgeStrength ?? 0.25f;
+ if (ImGui.DragFloat("Edge Strength", ref t3, 0.005f, 0f, 1f, "%.02f"))
+ this.style.EdgeStrength = t3;
+
+ t = this.style.Edge;
+ if (ImGui.Checkbox("Edge", ref t))
+ this.style.Edge = t;
+
+ ImGui.SameLine();
+ t = this.style.Bold;
+ if (ImGui.Checkbox("Bold", ref t))
+ this.style.Bold = t;
+
+ ImGui.SameLine();
+ t = this.style.Italic;
+ if (ImGui.Checkbox("Italic", ref t))
+ this.style.Italic = t;
+
+ ImGui.SameLine();
+ t = this.style.Shadow;
+ if (ImGui.Checkbox("Shadow", ref t))
+ this.style.Shadow = t;
+
+ ImGui.SameLine();
+ t = this.style.LinkUnderlineThickness > 0f;
+ if (ImGui.Checkbox("Link Underline", ref t))
+ this.style.LinkUnderlineThickness = t ? 1f : 0f;
+
+ ImGui.SameLine();
+ t = this.style.WrapWidth is null;
+ if (ImGui.Checkbox("Word Wrap", ref t))
+ this.style.WrapWidth = t ? null : float.PositiveInfinity;
+
+ ImGui.SameLine();
+ t = this.interactable;
+ if (ImGui.Checkbox("Interactable", ref t))
+ this.interactable = t;
+
+ if (ImGui.CollapsingHeader("UIColor Preview"))
+ {
+ if (this.uicolor is null)
+ {
+ var tt = new SeStringBuilder();
+ foreach (var uc in Service.Get().GetExcelSheet()!)
+ {
+ tt.Append($"#{uc.RowId}: ")
+ .BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(uc.RowId).EndMacro()
+ .Append("Edge ")
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(uc.RowId).EndMacro()
+ .Append("Edge+Color ")
+ .BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(0).EndMacro()
+ .Append("Color ")
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(0).EndMacro();
+ if (uc.RowId >= 500)
+ {
+ if (uc.RowId % 2 == 0)
+ {
+ tt.BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(uc.RowId).EndMacro()
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(uc.RowId + 1).EndMacro()
+ .Append($" => color#{uc.RowId + 1} + edge#{uc.RowId}")
+ .BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(0).EndMacro()
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(0).EndMacro();
+ }
+ else
+ {
+ tt.BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(uc.RowId).EndMacro()
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(uc.RowId - 1).EndMacro()
+ .Append($" => color#{uc.RowId - 1} + edge#{uc.RowId}")
+ .BeginMacro(MacroCode.EdgeColorType).AppendUIntExpression(0).EndMacro()
+ .BeginMacro(MacroCode.ColorType).AppendUIntExpression(0).EndMacro();
+ }
+ }
+
+ tt.BeginMacro(MacroCode.NewLine).EndMacro();
+ }
+
+ this.uicolor = tt.ToReadOnlySeString();
+ }
+
+ ImGuiHelpers.SeStringWrapped(this.uicolor.Value.Data.Span, this.style);
+ }
+
+ if (ImGui.CollapsingHeader("LogKind Preview"))
+ {
+ if (this.logkind is null)
+ {
+ var tt = new SeStringBuilder();
+ foreach (var uc in Service.Get().GetExcelSheet()!)
+ {
+ var ucsp = uc.Format.AsReadOnly().AsSpan();
+ if (ucsp.IsEmpty)
+ continue;
+
+ tt.Append($"#{uc.RowId}: ");
+ foreach (var p in ucsp.GetOffsetEnumerator())
+ {
+ if (p.Payload.Type == ReadOnlySePayloadType.Macro && p.Payload.MacroCode == MacroCode.String)
+ {
+ tt.Append("Text"u8);
+ continue;
+ }
+
+ tt.Append(new ReadOnlySeStringSpan(ucsp.Data.Slice(p.Offset, p.Payload.EnvelopeByteLength)));
+ }
+
+ tt.BeginMacro(MacroCode.NewLine).EndMacro();
+ }
+
+ this.logkind = tt.ToReadOnlySeString();
+ }
+
+ ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style);
+ }
+
+ if (ImGui.CollapsingHeader("Addon Table"))
+ {
+ this.addons ??= Service.Get().GetExcelSheet()!.ToArray();
+ if (ImGui.BeginTable("Addon Sheet", 3))
+ {
+ ImGui.TableSetupScrollFreeze(0, 1);
+ ImGui.TableSetupColumn("Row ID", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("0000000").X);
+ ImGui.TableSetupColumn("Text", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn(
+ "Misc",
+ ImGuiTableColumnFlags.WidthFixed,
+ ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA").X);
+ ImGui.TableHeadersRow();
+
+ var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ clipper.Begin(this.addons.Length);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ ImGui.TableNextRow();
+ ImGui.PushID(i);
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted($"{this.addons[i].RowId}");
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGuiHelpers.SeStringWrapped(this.addons[i].Text.AsReadOnly(), this.style);
+
+ ImGui.TableNextColumn();
+ if (ImGui.Button("Print to Chat"))
+ Service.Get().Print(this.addons[i].Text.ToDalamudString());
+
+ ImGui.PopID();
+ }
+ }
+
+ clipper.Destroy();
+ ImGui.EndTable();
+ }
+ }
+
if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed)
{
this.testStringBuffer.Dispose();
this.testStringBuffer = ImVectorWrapper.CreateFromSpan(
- "
Lorem ipsum dolor sit amet, conse<->ctetur adipi<->scing elit. Maece<->nas digni<->ssim sem at inter<->dum ferme<->ntum. Praes<->ent ferme<->ntum conva<->llis velit sit amet hendr<->erit. Sed eu nibh magna. Integ<->er nec lacus in velit porta euism<->od sed et lacus. Sed non mauri<->s venen<->atis, matti<->s metus in, aliqu<->et dolor. Aliqu<->am erat volut<->pat. Nulla venen<->atis velit ac susci<->pit euism<->od. suspe<->ndisse maxim<->us viver<->ra dui id dapib<->us. Nam torto<->r dolor, eleme<->ntum quis orci id, pulvi<->nar fring<->illa quam. Pelle<->ntesque laore<->et viver<->ra torto<->r eget matti<->s. Vesti<->bulum eget porta ante, a