using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Utility; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; namespace Dalamud.Interface.ImGuiSeStringRenderer; /// Calculated values from using ImGui styles. [StructLayout(LayoutKind.Sequential)] public unsafe ref struct SeStringDrawState { private static readonly int ChannelCount = Enum.GetValues().Length; private readonly ImDrawList* drawList; private ImDrawListSplitter splitter; /// Initializes a new instance of the struct. /// Raw SeString byte span. /// Instance of to initialize from. /// Instance of to use. /// Fragments. /// Font to use. internal SeStringDrawState( ReadOnlySpan span, scoped in SeStringDrawParams ssdp, SeStringColorStackSet colorStackSet, List fragments, ImFont* font) { this.Span = span; this.ColorStackSet = colorStackSet; this.Fragments = fragments; this.Font = font; if (ssdp.TargetDrawList is null) { if (!ThreadSafety.IsMainThread) { throw new ArgumentException( $"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread."); } this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList(); this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X; this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text); this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered); this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive); this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType; } else { this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = Vector2.Zero; this.FontSize = ssdp.FontSize ?? throw new ArgumentException( $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; this.Color = ssdp.Color ?? uint.MaxValue; this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread. this.ThemeIndex = ssdp.ThemeIndex ?? 0; } this.splitter = default; this.GetEntity = ssdp.GetEntity; this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y)); this.FontSizeScale = this.FontSize / this.Font->FontSize; this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight); this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f; this.Opacity = ssdp.EffectiveOpacity; this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity; this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000; this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000; this.ForceEdgeColor = ssdp.ForceEdgeColor; this.Bold = ssdp.Bold; this.Italic = ssdp.Italic; this.Edge = ssdp.Edge; this.Shadow = ssdp.Shadow; this.ColorStackSet.Initialize(ref this); fragments.Clear(); } /// public readonly ImDrawListPtr DrawList => new(this.drawList); /// Gets the raw SeString byte span. public ReadOnlySpan Span { get; } /// public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; } /// public Vector2 ScreenOffset { get; } /// public ImFont* Font { get; } /// public float FontSize { get; } /// Gets the multiplier value for glyph metrics, so that it scales to . /// Multiplied to , /// , and distance values from /// . public float FontSizeScale { get; } /// public float LineHeight { get; } /// public float WrapWidth { get; } /// public float LinkUnderlineThickness { get; } /// public float Opacity { get; } /// public float EdgeOpacity { get; } /// public int ThemeIndex { get; } /// public uint Color { get; set; } /// public uint EdgeColor { get; set; } /// public uint ShadowColor { get; set; } /// public uint LinkHoverBackColor { get; } /// public uint LinkActiveBackColor { get; } /// public bool ForceEdgeColor { get; } /// public bool Bold { get; set; } /// public bool Italic { get; set; } /// public bool Edge { get; set; } /// public bool Shadow { get; set; } /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawEdge => (this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000; /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 }; /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 }; /// Gets the color stacks. internal SeStringColorStackSet ColorStackSet { get; } /// Gets the text fragments. internal List Fragments { get; } /// Sets the current channel in the ImGui draw list splitter. /// Channel to switch to. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetCurrentChannel(SeStringDrawChannel channelIndex) => this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex); /// Draws a single texture. /// ImGui texture ID to draw from. /// Offset of the glyph in pixels w.r.t. . /// Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list. /// 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 in RGBA. public readonly void Draw( ImTextureID igTextureId, Vector2 offset, Vector2 size, Vector2 uv0, Vector2 uv1, uint color = uint.MaxValue) { offset += this.ScreenOffset; this.DrawList.AddImageQuad( igTextureId, offset, offset + size with { X = 0 }, offset + size, offset + size with { Y = 0 }, new(uv0.X, uv0.Y), new(uv0.X, uv1.Y), new(uv1.X, uv1.Y), new(uv1.X, uv0.Y), color); } /// Draws a single texture. /// ImGui texture ID to draw from. /// Offset of the glyph in pixels w.r.t. . /// 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. /// 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 in RGBA. /// Transformation for and that will push /// top and bottom pixels to apply faux italicization by and /// respectively. public readonly void Draw( ImTextureID igTextureId, Vector2 offset, Vector2 xy0, Vector2 xy1, Vector2 uv0, Vector2 uv1, uint color = uint.MaxValue, Vector2 dyItalic = default) { 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); } /// Draws a single glyph using current styling configurations. /// Glyph to draw. /// Offset of the glyph in pixels w.r.t. . internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) { var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID; var xy0 = new Vector2( MathF.Round(g.X0 * this.FontSizeScale), MathF.Round(g.Y0 * this.FontSizeScale)); var xy1 = new Vector2( MathF.Round(g.X1 * this.FontSizeScale), MathF.Round(g.Y1 * this.FontSizeScale)); var dxBold = this.Bold ? 2 : 1; var dyItalic = this.Italic ? new Vector2(this.FontSize - xy0.Y, this.FontSize - xy1.Y) / 6 : Vector2.Zero; // Note: dyItalic values can be non-rounded; the glyph will be rendered sheared anyway. offset.Y += MathF.Round((this.LineHeight - this.FontSize) / 2f); if (this.ShouldDrawShadow) { this.SetCurrentChannel(SeStringDrawChannel.Shadow); for (var i = 0; i < dxBold; i++) this.Draw(texId, offset + new Vector2(i, 1), xy0, xy1, g.UV0, g.UV1, this.ShadowColor, dyItalic); } if (this.ShouldDrawEdge) { this.SetCurrentChannel(SeStringDrawChannel.Edge); // Top & Bottom for (var i = -1; i <= dxBold; i++) { this.Draw(texId, offset + new Vector2(i, -1), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic); this.Draw(texId, offset + new Vector2(i, 1), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic); } // Left & Right this.Draw(texId, offset + new Vector2(-1, 0), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic); this.Draw(texId, offset + new Vector2(1, 0), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic); } if (this.ShouldDrawForeground) { this.SetCurrentChannel(SeStringDrawChannel.Foreground); for (var i = 0; i < dxBold; i++) this.Draw(texId, offset + new Vector2(i, 0), xy0, xy1, g.UV0, g.UV1, this.Color, dyItalic); } } /// Draws an underline, for links. /// Offset of the glyph in pixels w.r.t. /// . /// Advance width of the glyph. internal void DrawLinkUnderline(Vector2 offset, float advanceWidth) { if (this.LinkUnderlineThickness < 1f) return; offset += this.ScreenOffset; offset.Y += (this.LinkUnderlineThickness - 1) / 2f; offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font->Ascent * this.FontSizeScale)); this.SetCurrentChannel(SeStringDrawChannel.Foreground); this.DrawList.AddLine( offset, offset + new Vector2(advanceWidth, 0), this.Color, this.LinkUnderlineThickness); if (this is { Shadow: true, ShadowColor: >= 0x1000000 }) { this.SetCurrentChannel(SeStringDrawChannel.Shadow); this.DrawList.AddLine( offset + new Vector2(0, 1), offset + new Vector2(advanceWidth, 1), this.ShadowColor, this.LinkUnderlineThickness); } } /// 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 /// . internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune) { var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue ? this.Font->FindGlyph((ushort)rune.Value) : this.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 /// . internal 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. internal readonly float CalculateScaledDistance(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( this.Font->GetDistanceAdjustmentForPair( (ushort)left.Value, (ushort)right.Value) * this.FontSizeScale); } /// Handles style adjusting payloads. /// Payload to handle. /// true if the payload was handled. internal bool HandleStyleAdjustingPayloads(ReadOnlySePayloadSpan payload) { switch (payload.MacroCode) { case MacroCode.Color: this.ColorStackSet.HandleColorPayload(ref this, payload); return true; case MacroCode.EdgeColor: this.ColorStackSet.HandleEdgeColorPayload(ref this, payload); return true; case MacroCode.ShadowColor: this.ColorStackSet.HandleShadowColorPayload(ref this, payload); return true; case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): // doesn't actually work in chat log this.Bold = u != 0; return true; case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): this.Italic = u != 0; return true; case MacroCode.Edge when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): this.Edge = u != 0; return true; case MacroCode.Shadow when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): this.Shadow = u != 0; return true; case MacroCode.ColorType: this.ColorStackSet.HandleColorTypePayload(ref this, payload); return true; case MacroCode.EdgeColorType: this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload); return true; default: return false; } } /// Splits the draw list. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount); /// Merges the draw list. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void MergeDrawList() => this.splitter.Merge(this.drawList); }