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);
}