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 molli<->s nulla. Curab<->itur a ligul<->a leo. Aliqu<->am volut<->pat sagit<->tis dapib<->us.\n\nFusce iacul<->is aliqu<->am mi, eget portt<->itor arcu solli<->citudin conse<->ctetur. suspe<->ndisse aliqu<->am commo<->do tinci<->dunt. Duis sed posue<->re tellu<->s. Sed phare<->tra ex vel torto<->r pelle<->ntesque, inter<->dum porta sapie<->n digni<->ssim. Queue Dun Scait<->h. Cras aliqu<->et at nulla quis moles<->tie. Vesti<->bulum eu ligul<->a sapie<->n. Curab<->itur digni<->ssim feugi<->at volut<->pat.\n\nVesti<->bulum condi<->mentum laore<->et rhonc<->us. Vivam<->us et accum<->san purus. Curab<->itur inter<->dum vel ligul<->a ac euism<->od. Donec sed nisl digni<->ssim est tinci<->dunt iacul<->is. Praes<->ent hendr<->erit pelle<->ntesque nisl, quis lacin<->ia arcu dictu<->m sit amet. Aliqu<->am variu<->s lectu<->s vel mauri<->s imper<->diet posue<->re. Ut gravi<->da non sapie<->n sed hendr<->erit.\n\nProin quis dapib<->us odio. Cras sagit<->tis non sem sed porta. Donec iacul<->is est ligul<->a, digni<->ssim aliqu<->et augue matti<->s vitae. Duis ullam<->corper tempu<->s odio, non vesti<->bulum est biben<->dum quis. In purus elit, vehic<->ula tinci<->dunt dictu<->m in, aucto<->r nec enim. Curab<->itur a nisi in leo matti<->s pelle<->ntesque id nec sem. Nunc vel ultri<->ces nisl. Nam congu<->e vulpu<->tate males<->uada. Aenea<->n vesti<->bulum mauri<->s leo, sit amet iacul<->is est imper<->diet ut. Phase<->llus nec lobor<->tis lacus, sit amet scele<->risque purus. Nam id lacin<->ia velit, euism<->od feugi<->at dui. Nulla sodal<->es odio ligul<->a, et hendr<->erit torto<->r maxim<->us eu. Donec et sem eu magna volut<->pat accum<->san non ut lectu<->s.\n\nVivam<->us susci<->pit ferme<->ntum gravi<->da. Cras nec conse<->ctetur magna. Vivam<->us ante massa, accum<->san sit amet felis et, tempu<->s iacul<->is ipsum. Pelle<->ntesque vitae nisi accum<->san, venen<->atis lectu<->s aucto<->r, aliqu<->et liber<->o. Nam nec imper<->diet justo. Vivam<->us ut vehic<->ula turpi<->s. Nunc lobor<->tis pelle<->ntesque urna, sit amet solli<->citudin nibh fauci<->bus in. Curab<->itur eu lobor<->tis lacus. Donec eu hendr<->erit diam, vitae cursu<->s odio. Cras eget scele<->risque mi.

"u8, + "\n\nLorem 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 molli<->s nulla. Curab<->itur a ligul<->a leo. Aliqu<->am volut<->pat sagit<->tis dapib<->us.\n\nFusce iacul<->is aliqu<->am mi, eget portt<->itor arcu solli<->citudin conse<->ctetur. suspe<->ndisse aliqu<->am commo<->do tinci<->dunt. Duis sed posue<->re tellu<->s. Sed phare<->tra ex vel torto<->r pelle<->ntesque, inter<->dum porta sapie<->n digni<->ssim. Queue Dun Scait<->h. Cras aliqu<->et at nulla quis moles<->tie. Vesti<->bulum eu ligul<->a sapie<->n. Curab<->itur digni<->ssim feugi<->at volut<->pat.\n\nVesti<->bulum condi<->mentum laore<->et rhonc<->us. Vivam<->us et accum<->san purus. Curab<->itur inter<->dum vel ligul<->a ac euism<->od. Donec sed nisl digni<->ssim est tinci<->dunt iacul<->is. Praes<->ent hendr<->erit pelle<->ntesque nisl, quis lacin<->ia arcu dictu<->m sit amet. Aliqu<->am variu<->s lectu<->s vel mauri<->s imper<->diet posue<->re. Ut gravi<->da non sapie<->n sed hendr<->erit.\n\nProin quis dapib<->us odio. Cras sagit<->tis non sem sed porta. Donec iacul<->is est ligul<->a, digni<->ssim aliqu<->et augue matti<->s vitae. Duis ullam<->corper tempu<->s odio, non vesti<->bulum est biben<->dum quis. In purus elit, vehic<->ula tinci<->dunt dictu<->m in, aucto<->r nec enim. Curab<->itur a nisi in leo matti<->s pelle<->ntesque id nec sem. Nunc vel ultri<->ces nisl. Nam congu<->e vulpu<->tate males<->uada. Aenea<->n vesti<->bulum mauri<->s leo, sit amet iacul<->is est imper<->diet ut. Phase<->llus nec lobor<->tis lacus, sit amet scele<->risque purus. Nam id lacin<->ia velit, euism<->od feugi<->at dui. Nulla sodal<->es odio ligul<->a, et hendr<->erit torto<->r maxim<->us eu. Donec et sem eu magna volut<->pat accum<->san non ut lectu<->s.\n\nVivam<->us susci<->pit ferme<->ntum gravi<->da. Cras nec conse<->ctetur magna. Vivam<->us ante massa, accum<->san sit amet felis et, tempu<->s iacul<->is ipsum. Pelle<->ntesque vitae nisi accum<->san, venen<->atis lectu<->s aucto<->r, aliqu<->et liber<->o. Nam nec imper<->diet justo. Vivam<->us ut vehic<->ula turpi<->s. Nunc lobor<->tis pelle<->ntesque urna, sit amet solli<->citudin nibh fauci<->bus in. Curab<->itur eu lobor<->tis lacus. Donec eu hendr<->erit diam, vitae cursu<->s odio. Cras eget scele<->risque mi.\n\n· Testing aaaaalink aaaaabbbb.\n· Open example.com\n· Open example.org\n\n\n\ncolortype502,edgecolortype503\n\nOpacity values are ignored:\nopacity FF\nopacity 80\nopacity 00\nTest 1\nTest 2\nWithout edgeShadowWith edge"u8, minCapacity: 65536); - this.testString = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan); + this.testString = string.Empty; } + ImGui.SameLine(); + + if (ImGui.Button("Print to Chat Log")) + { + fixed (byte* p = Service.Get().CompileAndCache(this.testString).Data.Span) + Service.Get().Print(Game.Text.SeStringHandling.SeString.Parse(p)); + } + + ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.CompileSeStringWrapped( + "· For ease of testing, line breaks are automatically replaced to \\
.", + this.style); + ImGuiHelpers.ScaledDummy(3); + fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -50,7 +281,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget null) != 0) { var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); - this.testString = Encoding.UTF8.GetString(this.testStringBuffer.StorageSpan[..len]); if (len + 4 >= this.testStringBuffer.Capacity) this.testStringBuffer.EnsureCapacityExponential(len + 4); if (len < this.testStringBuffer.Capacity) @@ -58,10 +288,28 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget this.testStringBuffer.LengthUnsafe = len; this.testStringBuffer.StorageSpan[len] = default; } + + this.testString = string.Empty; } } + if (this.testString == string.Empty && this.testStringBuffer.Length != 0) + this.testString = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan); + ImGui.Separator(); - ImGuiHelpers.CompileSeStringWrapped(this.testString); + if (this.interactable) + { + if (ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style, new("this is an ImGui id")) is + { InteractedPayload: { } payload, InteractedPayloadOffset: var offset, InteractedPayloadEnvelope: var envelope } rr) + { + ImGui.TextUnformatted($"Hovered[{offset}]: {new ReadOnlySeStringSpan(envelope).ToString()}; {payload}"); + if (rr.Clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp) + Util.OpenLink(dlp.ExtraString); + } + } + else + { + ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style); + } } } diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d979259ea..e64372700 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -9,6 +9,9 @@ using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; @@ -16,10 +19,6 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; -using Lumina.Text.ReadOnly; - -using SeStringRenderer = Dalamud.Interface.Internal.ImGuiSeStringRenderer.SeStringRenderer; - namespace Dalamud.Interface.Utility; /// @@ -181,13 +180,57 @@ public static class ImGuiHelpers if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}"); } - /// + /// Draws a SeString. + /// SeString to draw. + /// Wrapping width. If a non-positive number is provided, then the remainder of the width + /// will be used. + /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. + /// The function definition is stable; only in the next API version a function may be removed. public static void SeStringWrapped(ReadOnlySpan sss, float wrapWidth = 0) => - Service.Get().DrawWrapped(sss, wrapWidth); + Service.Get().Draw(sss, new() { WrapWidth = wrapWidth > 0 ? wrapWidth : null }); - /// + /// Creates and caches a SeString from a text macro representation, and then draws it. + /// SeString text macro representation. + /// Newline characters will be normalized to . + /// Wrapping width. If a non-positive number is provided, then the remainder of the width + /// will be used. + /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. + /// The function definition is stable; only in the next API version a function may be removed. public static void CompileSeStringWrapped(string text, float wrapWidth = 0) => - Service.Get().CompileAndDrawWrapped(text, wrapWidth); + Service.Get().CompileAndDrawWrapped( + text, + new() { WrapWidth = wrapWidth > 0 ? wrapWidth : null }); + + /// Draws a SeString. + /// SeString to draw. + /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. + /// Interaction result of the rendered text. + /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. + /// The function definition is stable; only in the next API version a function may be removed. + public static SeStringDrawResult SeStringWrapped( + ReadOnlySpan sss, + in SeStringDrawParams style = default, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) => + Service.Get().Draw(sss, style, imGuiId, buttonFlags); + + /// Creates and caches a SeString from a text macro representation, and then draws it. + /// SeString text macro representation. + /// Newline characters will be normalized to . + /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. + /// Interaction result of the rendered text. + /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. + /// The function definition is stable; only in the next API version a function may be removed. + public static SeStringDrawResult CompileSeStringWrapped( + string text, + in SeStringDrawParams style, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) => + Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags); /// /// Write unformatted text wrapped. diff --git a/Dalamud/Interface/Utility/ImGuiId.cs b/Dalamud/Interface/Utility/ImGuiId.cs new file mode 100644 index 000000000..0231f3749 --- /dev/null +++ b/Dalamud/Interface/Utility/ImGuiId.cs @@ -0,0 +1,176 @@ +using System.Runtime.CompilerServices; + +using ImGuiNET; + +namespace Dalamud.Interface.Utility; + +/// Represents any type of ImGui ID. +public readonly ref struct ImGuiId +{ + /// Type of the ID. + public readonly Type IdType; + + /// Numeric ID. Valid if is . + public readonly nint Numeric; + + /// UTF-16 string ID. Valid if is . + public readonly ReadOnlySpan U16; + + /// UTF-8 string ID. Valid if is . + public readonly ReadOnlySpan U8; + + /// Initializes a new instance of the struct. + /// A numeric ID, or 0 to not provide an ID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImGuiId(nint id) + { + if (id != 0) + (this.IdType, this.Numeric) = (Type.Numeric, id); + } + + /// Initializes a new instance of the struct. + /// A UTF-16 string ID, or to not provide an ID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImGuiId(ReadOnlySpan id) + { + if (!id.IsEmpty) + { + this.IdType = Type.U16; + this.U16 = id; + } + } + + /// Initializes a new instance of the struct. + /// A UTF-8 string ID, or to not provide an ID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImGuiId(ReadOnlySpan id) + { + if (!id.IsEmpty) + { + this.IdType = Type.U8; + this.U8 = id; + } + } + + /// Possible types for an ImGui ID. + public enum Type + { + /// No ID is specified. + None, + + /// field is used. + Numeric, + + /// field is used. + U16, + + /// field is used. + U8, + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe implicit operator ImGuiId(void* id) => new((nint)id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe implicit operator ImGuiId(float id) => new(*(int*)&id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe implicit operator ImGuiId(double id) => new(*(nint*)&id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(sbyte id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(byte id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(char id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(short id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(ushort id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(int id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(uint id) => new((nint)id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(nint id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(nuint id) => new((nint)id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(Span id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(ReadOnlySpan id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(Memory id) => new(id.Span); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(ReadOnlyMemory id) => new(id.Span); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(char[] id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(string id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(Span id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(ReadOnlySpan id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(Memory id) => new(id.Span); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(ReadOnlyMemory id) => new(id.Span); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ImGuiId(byte[] id) => new(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(ImGuiId id) => !id.IsEmpty(); + + /// Determines if no ID is stored. + /// true if no ID is stored. + public bool IsEmpty() => this.IdType switch + { + Type.None => true, + Type.Numeric => this.Numeric == 0, + Type.U16 => this.U16.IsEmpty, + Type.U8 => this.U8.IsEmpty, + _ => true, + }; + + /// Pushes ID if any is stored. + /// true if any ID is pushed. + public unsafe bool PushId() + { + switch (this.IdType) + { + case Type.Numeric: + ImGuiNative.igPushID_Ptr((void*)this.Numeric); + return true; + case Type.U16: + fixed (void* p = this.U16) + ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + (this.U16.Length * 2)); + return true; + case Type.U8: + fixed (void* p = this.U8) + ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + this.U8.Length); + return true; + case Type.None: + default: + return false; + } + } +} diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index 7eac9160f..545a7e8a8 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,4 +1,9 @@ -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface.ImGuiSeStringRenderer.Internal; + +using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; +using DSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; +using LSeString = Lumina.Text.SeString; +using LSeStringBuilder = Lumina.Text.SeStringBuilder; namespace Dalamud.Utility; @@ -13,7 +18,51 @@ public static class SeStringExtensions /// /// The original Lumina SeString. /// The re-parsed Dalamud SeString. - public static SeString ToDalamudString(this Lumina.Text.SeString originalString) => SeString.Parse(originalString.RawData); + public static DSeString ToDalamudString(this LSeString originalString) => DSeString.Parse(originalString.RawData); + + /// Compiles and appends a macro string. + /// Target SeString builder. + /// Macro string in UTF-8 to compile and append to . + /// this for method chaining. + /// Must be called from the main thread. + public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan macroString) + { + ThreadSafety.AssertMainThread(); + return ssb.Append(Service.Get().Compile(macroString)); + } + + /// Compiles and appends a macro string. + /// Target SeString builder. + /// Macro string in UTF-16 to compile and append to . + /// this for method chaining. + /// Must be called from the main thread. + public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan macroString) + { + ThreadSafety.AssertMainThread(); + return ssb.Append(Service.Get().Compile(macroString)); + } + + /// Compiles and appends a macro string. + /// Target SeString builder. + /// Macro string in UTF-8 to compile and append to . + /// this for method chaining. + /// Must be called from the main thread. + public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) + { + ThreadSafety.AssertMainThread(); + return ssb.Append(DSeString.Parse(Service.Get().Compile(macroString))); + } + + /// Compiles and appends a macro string. + /// Target SeString builder. + /// Macro string in UTF-16 to compile and append to . + /// this for method chaining. + /// Must be called from the main thread. + public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) + { + ThreadSafety.AssertMainThread(); + return ssb.Append(DSeString.Parse(Service.Get().Compile(macroString))); + } /// /// Validate if character name is valid. @@ -24,8 +73,5 @@ public static class SeStringExtensions /// /// character name to validate. /// indicator if character is name is valid. - public static bool IsValidCharacterName(this SeString value) - { - return value.ToString().IsValidCharacterName(); - } + public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName(); }