SeString renderer: Implement replacement entity (#1993)

* Refactor

* Implement replacement entity

* Apply rounding functions more correctly
This commit is contained in:
srkizer 2024-08-04 20:30:49 +09:00 committed by GitHub
parent 23a2bd6228
commit 878b96e67d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1199 additions and 672 deletions

View file

@ -2,6 +2,7 @@ using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Dalamud.Interface;
@ -247,6 +248,14 @@ public static class ColorHelpers
public static uint Desaturate(uint color, float amount)
=> RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount));
/// <summary>Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.</summary>
/// <param name="rgba">RGBA value to transform.</param>
/// <param name="opacity">Opacity to apply.</param>
/// <returns>Transformed value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint ApplyOpacity(uint rgba, float opacity) =>
((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
/// <summary>
/// Fade a color.
/// </summary>

View file

@ -145,5 +145,16 @@ internal sealed unsafe class GfdFile : FileResource
/// <summary>Gets the UV1 of the HQ version of this entry.</summary>
public Vector2 HqUv1 => new((this.Left + this.Width) / 256f, (this.Top + this.Height + 170.5f) / 512f);
/// <summary>Calculates the size in pixels of a GFD entry when drawn along with a text.</summary>
/// <param name="fontSize">Font size in pixels.</param>
/// <param name="useHq">Whether to draw the HQ texture.</param>
/// <returns>Determined size of the GFD entry when drawn.</returns>
public readonly Vector2 CalculateScaledSize(float fontSize, out bool useHq)
{
useHq = fontSize > 19;
var targetHeight = useHq ? fontSize : 20;
return new(this.Width * (targetHeight / this.Height), targetHeight);
}
}
}

View file

@ -0,0 +1,198 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets2;
using Lumina.Text.Expressions;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Color stacks to use while evaluating a SeString.</summary>
internal sealed class SeStringColorStackSet
{
/// <summary>Parsed <see cref="UIColor.UIForeground"/>, containing colors to use with
/// <see cref="MacroCode.ColorType"/>.</summary>
private readonly uint[] colorTypes;
/// <summary>Parsed <see cref="UIColor.UIGlow"/>, containing colors to use with
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
private readonly uint[] edgeColorTypes;
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> colorStack = [];
/// <summary>Edge/border color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> edgeColorStack = [];
/// <summary>Shadow color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> shadowColorStack = [];
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
/// <param name="uiColor">UIColor sheet.</param>
public SeStringColorStackSet(ExcelSheet<UIColor> uiColor)
{
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);
}
}
/// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary>
public bool HasAdditionalEdgeColor { get; private set; }
/// <summary>Resets the colors in the stack.</summary>
/// <param name="drawState">Draw state.</param>
internal void Initialize(scoped ref SeStringDrawState drawState)
{
this.colorStack.Clear();
this.edgeColorStack.Clear();
this.shadowColorStack.Clear();
this.colorStack.Add(drawState.Color);
this.edgeColorStack.Add(drawState.EdgeColor);
this.shadowColorStack.Add(drawState.ShadowColor);
drawState.Color = ColorHelpers.ApplyOpacity(drawState.Color, drawState.Opacity);
drawState.EdgeColor = ColorHelpers.ApplyOpacity(drawState.EdgeColor, drawState.EdgeOpacity);
drawState.ShadowColor = ColorHelpers.ApplyOpacity(drawState.ShadowColor, drawState.Opacity);
}
/// <summary>Handles a <see cref="MacroCode.Color"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
/// <param name="payload">Payload to handle.</param>
internal void HandleColorPayload(scoped ref SeStringDrawState drawState, ReadOnlySePayloadSpan payload) =>
drawState.Color = ColorHelpers.ApplyOpacity(AdjustStack(this.colorStack, payload), drawState.Opacity);
/// <summary>Handles a <see cref="MacroCode.EdgeColor"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
/// <param name="payload">Payload to handle.</param>
internal void HandleEdgeColorPayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload)
{
var newColor = AdjustStack(this.edgeColorStack, payload);
if (!drawState.ForceEdgeColor)
drawState.EdgeColor = ColorHelpers.ApplyOpacity(newColor, drawState.EdgeOpacity);
this.HasAdditionalEdgeColor = this.edgeColorStack.Count > 1;
}
/// <summary>Handles a <see cref="MacroCode.ShadowColor"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
/// <param name="payload">Payload to handle.</param>
internal void HandleShadowColorPayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload) =>
drawState.ShadowColor = ColorHelpers.ApplyOpacity(AdjustStack(this.shadowColorStack, payload), drawState.Opacity);
/// <summary>Handles a <see cref="MacroCode.ColorType"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
/// <param name="payload">Payload to handle.</param>
internal void HandleColorTypePayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload) =>
drawState.Color = ColorHelpers.ApplyOpacity(AdjustStack(this.colorStack, this.colorTypes, payload), drawState.Opacity);
/// <summary>Handles a <see cref="MacroCode.EdgeColorType"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
/// <param name="payload">Payload to handle.</param>
internal void HandleEdgeColorTypePayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload)
{
var newColor = AdjustStack(this.edgeColorStack, this.edgeColorTypes, payload);
if (!drawState.ForceEdgeColor)
drawState.EdgeColor = ColorHelpers.ApplyOpacity(newColor, drawState.EdgeOpacity);
this.HasAdditionalEdgeColor = this.edgeColorStack.Count > 1;
}
/// <summary>Swaps red and blue channels of a given color in ARGB(BB GG RR AA) and ABGR(RR GG BB AA).</summary>
/// <param name="x">Color to process.</param>
/// <returns>Swapped color.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
private static unsafe uint AdjustStack(List<uint> 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];
}
private static uint AdjustStack(List<uint> 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];
}
}

View file

@ -12,9 +12,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
internal ref struct LineBreakEnumerator
{
private readonly UtfEnumeratorFlags enumeratorFlags;
private readonly int dataLength;
private UtfEnumerator enumerator;
private int dataLength;
private int currentByteOffsetDelta;
private Entry class1;
private Entry class2;
@ -24,8 +26,6 @@ internal ref struct LineBreakEnumerator
private int consecutiveRegionalIndicators;
private bool finished;
/// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary>
/// <param name="data">UTF-N byte sequence.</param>
/// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param>
@ -58,11 +58,25 @@ internal ref struct LineBreakEnumerator
/// <inheritdoc cref="IEnumerator{T}.Current"/>
public (int ByteOffset, bool Mandatory) Current { get; private set; }
/// <summary>Gets a value indicating whether the end of the underlying span has been reached.</summary>
public bool Finished { get; private set; }
/// <summary>Resumes enumeration with the given data.</summary>
/// <param name="data">The data.</param>
/// <param name="offsetDelta">Offset to add to <see cref="Current"/>.<c>ByteOffset</c>.</param>
public void ResumeWith(ReadOnlySpan<byte> data, int offsetDelta)
{
this.enumerator = UtfEnumerator.From(data, this.enumeratorFlags);
this.dataLength = data.Length;
this.currentByteOffsetDelta = offsetDelta;
this.Finished = false;
}
/// <inheritdoc cref="IEnumerator.MoveNext"/>
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
public bool MoveNext()
{
if (this.finished)
if (this.Finished)
return false;
while (this.enumerator.MoveNext())
@ -77,10 +91,10 @@ internal ref struct LineBreakEnumerator
switch (this.HandleCharacter(effectiveInt))
{
case LineBreakMode.Mandatory:
this.Current = (this.enumerator.Current.ByteOffset, true);
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, true);
return true;
case LineBreakMode.Optional:
this.Current = (this.enumerator.Current.ByteOffset, false);
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, false);
return true;
case LineBreakMode.Prohibited:
default:
@ -90,8 +104,8 @@ internal ref struct LineBreakEnumerator
// Start and end of text:
// LB3 Always break at the end of text.
this.Current = (this.dataLength, true);
this.finished = true;
this.Current = (this.dataLength + this.currentByteOffsetDelta, true);
this.Finished = true;
return true;
}

View file

@ -0,0 +1,42 @@
namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// <summary>Predefined channels for drawing onto, for out-of-order drawing.</summary>
// Notes: values must be consecutively increasing, starting from 0. Higher values has higher priority.
public enum SeStringDrawChannel
{
/// <summary>Next draw operation on the draw list will be put below <see cref="Background"/>.</summary>
BelowBackground,
/// <summary>Next draw operation on the draw list will be put onto the background channel.</summary>
Background,
/// <summary>Next draw operation on the draw list will be put above <see cref="Background"/>.</summary>
AboveBackground,
/// <summary>Next draw operation on the draw list will be put below <see cref="Shadow"/>.</summary>
BelowShadow,
/// <summary>Next draw operation on the draw list will be put onto the shadow channel.</summary>
Shadow,
/// <summary>Next draw operation on the draw list will be put above <see cref="Shadow"/>.</summary>
AboveShadow,
/// <summary>Next draw operation on the draw list will be put below <see cref="Edge"/>.</summary>
BelowEdge,
/// <summary>Next draw operation on the draw list will be put onto the edge channel.</summary>
Edge,
/// <summary>Next draw operation on the draw list will be put above <see cref="Edge"/>.</summary>
AboveEdge,
/// <summary>Next draw operation on the draw list will be put below <see cref="Foreground"/>.</summary>
BelowForeground,
/// <summary>Next draw operation on the draw list will be put onto the foreground channel.</summary>
Foreground,
/// <summary>Next draw operation on the draw list will be put above <see cref="Foreground"/>.</summary>
AboveForeground,
}

View file

@ -1,6 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.InteropServices;
using ImGuiNET;
@ -18,14 +16,18 @@ public record struct SeStringDrawParams
/// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; }
/// <summary>Gets or sets the font to use.</summary>
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value>
public ImFontPtr? Font { get; set; }
/// <summary>Gets or sets the function to be called on every codepoint and payload for the purpose of offering
/// chances to draw something else instead of glyphs or SeString payload entities.</summary>
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
/// <summary>Gets or sets the screen offset of the left top corner.</summary>
/// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos"/>.</value>
public Vector2? ScreenOffset { get; set; }
/// <summary>Gets or sets the font to use.</summary>
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value>
public ImFontPtr? Font { get; set; }
/// <summary>Gets or sets the font size.</summary>
/// <value>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>.
/// </value>
@ -86,83 +88,23 @@ public record struct SeStringDrawParams
public bool Italic { get; set; }
/// <summary>Gets or sets a value indicating whether the text is rendered with edge.</summary>
/// <remarks>If an edge color is pushed with <see cref="MacroCode.EdgeColor"/> or
/// <see cref="MacroCode.EdgeColorType"/>, it will be drawn regardless. Set <see cref="ForceEdgeColor"/> to
/// <c>true</c> and set <see cref="EdgeColor"/> to <c>0</c> to fully disable edge.</remarks>
public bool Edge { get; set; }
/// <summary>Gets or sets a value indicating whether the text is rendered with shadow.</summary>
public bool Shadow { get; set; }
private readonly unsafe ImFont* EffectiveFont =>
/// <summary>Gets the effective font.</summary>
internal 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);
/// <summary>Gets the effective line height in pixels.</summary>
internal readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
private readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
/// <summary>Calculated values from <see cref="SeStringDrawParams"/> using ImGui styles.</summary>
[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)
{
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
public readonly ImDrawList* DrawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
/// <inheritdoc cref="SeStringDrawParams.Font"/>
public readonly ImFont* Font = ssdp.EffectiveFont;
/// <inheritdoc cref="SeStringDrawParams.ScreenOffset"/>
public readonly Vector2 ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
/// <inheritdoc cref="SeStringDrawParams.FontSize"/>
public readonly float FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
/// <inheritdoc cref="SeStringDrawParams.LineHeight"/>
public readonly float LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
/// <inheritdoc cref="SeStringDrawParams.WrapWidth"/>
public readonly float WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
/// <inheritdoc cref="SeStringDrawParams.LinkUnderlineThickness"/>
public readonly float LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
/// <inheritdoc cref="SeStringDrawParams.Opacity"/>
public readonly float Opacity = ssdp.EffectiveOpacity;
/// <inheritdoc cref="SeStringDrawParams.EdgeStrength"/>
public readonly float EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
/// <inheritdoc cref="SeStringDrawParams.Color"/>
public uint Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
/// <inheritdoc cref="SeStringDrawParams.EdgeColor"/>
public uint EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
/// <inheritdoc cref="SeStringDrawParams.ShadowColor"/>
public uint ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
/// <inheritdoc cref="SeStringDrawParams.LinkHoverBackColor"/>
public readonly uint LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
/// <inheritdoc cref="SeStringDrawParams.LinkActiveBackColor"/>
public readonly uint LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
/// <inheritdoc cref="SeStringDrawParams.ForceEdgeColor"/>
public readonly bool ForceEdgeColor = ssdp.ForceEdgeColor;
/// <inheritdoc cref="SeStringDrawParams.Bold"/>
public bool Bold = ssdp.Bold;
/// <inheritdoc cref="SeStringDrawParams.Italic"/>
public bool Italic = ssdp.Italic;
/// <inheritdoc cref="SeStringDrawParams.Edge"/>
public bool Edge = ssdp.Edge;
/// <inheritdoc cref="SeStringDrawParams.Shadow"/>
public bool Shadow = ssdp.Shadow;
}
/// <summary>Gets the effective opacity.</summary>
internal readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
}

View file

@ -0,0 +1,400 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// <summary>Calculated values from <see cref="SeStringDrawParams"/> using ImGui styles.</summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe ref struct SeStringDrawState
{
private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length;
private readonly ImDrawList* drawList;
private readonly SeStringColorStackSet colorStackSet;
private readonly ImDrawListSplitter* splitter;
/// <summary>Initializes a new instance of the <see cref="SeStringDrawState"/> struct.</summary>
/// <param name="span">Raw SeString byte span.</param>
/// <param name="ssdp">Instance of <see cref="SeStringDrawParams"/> to initialize from.</param>
/// <param name="colorStackSet">Instance of <see cref="SeStringColorStackSet"/> to use.</param>
/// <param name="splitter">Instance of ImGui Splitter to use.</param>
internal SeStringDrawState(
ReadOnlySpan<byte> span,
scoped in SeStringDrawParams ssdp,
SeStringColorStackSet colorStackSet,
ImDrawListSplitter* splitter)
{
this.colorStackSet = colorStackSet;
this.splitter = splitter;
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.Span = span;
this.GetEntity = ssdp.GetEntity;
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
this.Font = ssdp.EffectiveFont;
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.FontSizeScale = this.FontSize / this.Font->FontSize;
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
this.Opacity = ssdp.EffectiveOpacity;
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ForceEdgeColor = ssdp.ForceEdgeColor;
this.Bold = ssdp.Bold;
this.Italic = ssdp.Italic;
this.Edge = ssdp.Edge;
this.Shadow = ssdp.Shadow;
}
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
public readonly ImDrawListPtr DrawList => new(this.drawList);
/// <summary>Gets the raw SeString byte span.</summary>
public ReadOnlySpan<byte> Span { get; }
/// <inheritdoc cref="SeStringDrawParams.GetEntity"/>
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; }
/// <inheritdoc cref="SeStringDrawParams.ScreenOffset"/>
public Vector2 ScreenOffset { get; }
/// <inheritdoc cref="SeStringDrawParams.Font"/>
public ImFont* Font { get; }
/// <inheritdoc cref="SeStringDrawParams.FontSize"/>
public float FontSize { get; }
/// <summary>Gets the multiplier value for glyph metrics, so that it scales to <see cref="FontSize"/>.</summary>
/// <remarks>Multiplied to <see cref="ImGuiHelpers.ImFontGlyphReal.XY"/>,
/// <see cref="ImGuiHelpers.ImFontGlyphReal.AdvanceX"/>, and distance values from
/// <see cref="ImFontPtr.GetDistanceAdjustmentForPair"/>.</remarks>
public float FontSizeScale { get; }
/// <inheritdoc cref="SeStringDrawParams.LineHeight"/>
public float LineHeight { get; }
/// <inheritdoc cref="SeStringDrawParams.WrapWidth"/>
public float WrapWidth { get; }
/// <inheritdoc cref="SeStringDrawParams.LinkUnderlineThickness"/>
public float LinkUnderlineThickness { get; }
/// <inheritdoc cref="SeStringDrawParams.Opacity"/>
public float Opacity { get; }
/// <inheritdoc cref="SeStringDrawParams.EdgeStrength"/>
public float EdgeOpacity { get; }
/// <inheritdoc cref="SeStringDrawParams.Color"/>
public uint Color { get; set; }
/// <inheritdoc cref="SeStringDrawParams.EdgeColor"/>
public uint EdgeColor { get; set; }
/// <inheritdoc cref="SeStringDrawParams.ShadowColor"/>
public uint ShadowColor { get; set; }
/// <inheritdoc cref="SeStringDrawParams.LinkHoverBackColor"/>
public uint LinkHoverBackColor { get; }
/// <inheritdoc cref="SeStringDrawParams.LinkActiveBackColor"/>
public uint LinkActiveBackColor { get; }
/// <inheritdoc cref="SeStringDrawParams.ForceEdgeColor"/>
public bool ForceEdgeColor { get; }
/// <inheritdoc cref="SeStringDrawParams.Bold"/>
public bool Bold { get; set; }
/// <inheritdoc cref="SeStringDrawParams.Italic"/>
public bool Italic { get; set; }
/// <inheritdoc cref="SeStringDrawParams.Edge"/>
public bool Edge { get; set; }
/// <inheritdoc cref="SeStringDrawParams.Shadow"/>
public bool Shadow { get; set; }
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawEdge =>
(this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
/// <param name="channelIndex">Channel to switch to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
ImGuiNative.ImDrawListSplitter_SetCurrentChannel(this.splitter, this.drawList, (int)channelIndex);
/// <summary>Draws a single texture.</summary>
/// <param name="igTextureId">ImGui texture ID to draw from.</param>
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
/// <param name="size">Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.</param>
/// <param name="uv0">Left top corner of the glyph w.r.t. its glyph origin in the source texture.</param>
/// <param name="uv1">Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.</param>
/// <param name="color">Color of the glyph in RGBA.</param>
public readonly void Draw(
nint 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);
}
/// <summary>Draws a single texture.</summary>
/// <param name="igTextureId">ImGui texture ID to draw from.</param>
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
/// <param name="xy0">Left top corner of the glyph w.r.t. its glyph origin in the target draw list.</param>
/// <param name="xy1">Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.</param>
/// <param name="uv0">Left top corner of the glyph w.r.t. its glyph origin in the source texture.</param>
/// <param name="uv1">Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.</param>
/// <param name="color">Color of the glyph in RGBA.</param>
/// <param name="dyItalic">Transformation for <paramref name="xy0"/> and <paramref name="xy1"/> that will push
/// top and bottom pixels to apply faux italicization by <see cref="Vector2.X"/> and <see cref="Vector2.Y"/>
/// respectively.</param>
public readonly void Draw(
nint 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);
}
/// <summary>Draws a single glyph using current styling configurations.</summary>
/// <param name="g">Glyph to draw.</param>
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
{
var texId = this.Font->ContainerAtlas->Textures.Ref<ImFontAtlasTexture>(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);
}
}
/// <summary>Draws an underline, for links.</summary>
/// <param name="offset">Offset of the glyph in pixels w.r.t.
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="advanceWidth">Advance width of the glyph.</param>
internal readonly 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);
}
}
/// <summary>Gets the glyph corresponding to the given codepoint.</summary>
/// <param name="rune">An instance of <see cref="Rune"/> that represents a character to display.</param>
/// <returns>Corresponding glyph, or glyph of a fallback character specified from
/// <see cref="ImFont.FallbackChar"/>.</returns>
internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
{
var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
? ImGuiNative.ImFont_FindGlyph(this.Font, (ushort)rune.Value)
: this.Font->FallbackGlyph;
return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
}
/// <summary>Gets the glyph corresponding to the given codepoint.</summary>
/// <param name="rune">An instance of <see cref="Rune"/> 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.</param>
/// <returns>Corresponding glyph, or glyph of a fallback character specified from
/// <see cref="ImFont.FallbackChar"/>.</returns>
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;
}
/// <summary>Gets the kerning adjustment between two glyphs in a succession corresponding to the given runes.
/// </summary>
/// <param name="left">Rune representing the glyph on the left side of a pair.</param>
/// <param name="right">Rune representing the glyph on the right side of a pair.</param>
/// <returns>Distance adjustment in pixels, scaled to the size specified from
/// <see cref="SeStringDrawParams.FontSize"/>, and rounded.</returns>
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(
ImGuiNative.ImFont_GetDistanceAdjustmentForPair(
this.Font,
(ushort)left.Value,
(ushort)right.Value) * this.FontSizeScale);
}
/// <summary>Handles style adjusting payloads.</summary>
/// <param name="payload">Payload to handle.</param>
/// <returns><c>true</c> if the payload was handled.</returns>
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;
}
}
/// <summary>Splits the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void SplitDrawList() =>
ImGuiNative.ImDrawListSplitter_Split(this.splitter, this.drawList, ChannelCount);
/// <summary>Merges the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void MergeDrawList() => ImGuiNative.ImDrawListSplitter_Merge(this.splitter, this.drawList);
}

View file

@ -0,0 +1,48 @@
using System.Numerics;
namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// <summary>Replacement entity to draw instead while rendering a SeString.</summary>
public readonly record struct SeStringReplacementEntity
{
/// <summary>Initializes a new instance of the <see cref="SeStringReplacementEntity"/> struct.</summary>
/// <param name="byteLength">Number of bytes taken by this entity. Must be at least 0. If <c>0</c>, then the entity
/// is considered as empty.</param>
/// <param name="size">Size of this entity in pixels. Components must be non-negative.</param>
/// <param name="draw">Draw callback.</param>
public SeStringReplacementEntity(int byteLength, Vector2 size, DrawDelegate draw)
{
ArgumentOutOfRangeException.ThrowIfNegative(byteLength);
ArgumentOutOfRangeException.ThrowIfNegative(size.X, nameof(size));
ArgumentOutOfRangeException.ThrowIfNegative(size.Y, nameof(size));
ArgumentNullException.ThrowIfNull(draw);
this.ByteLength = byteLength;
this.Size = size;
this.Draw = draw;
}
/// <summary>Gets the replacement entity.</summary>
/// <param name="state">Draw state.</param>
/// <param name="byteOffset">Byte offset in <see cref="SeStringDrawState.Span"/>.</param>
/// <returns>Replacement entity definition, or <c>default</c> if none.</returns>
public delegate SeStringReplacementEntity GetEntityDelegate(scoped in SeStringDrawState state, int byteOffset);
/// <summary>Draws the replacement entity.</summary>
/// <param name="state">Draw state.</param>
/// <param name="byteOffset">Byte offset in <see cref="SeStringDrawState.Span"/>.</param>
/// <param name="offset">Relative offset in pixels w.r.t. <see cref="SeStringDrawState.ScreenOffset"/>.</param>
public delegate void DrawDelegate(scoped in SeStringDrawState state, int byteOffset, Vector2 offset);
/// <summary>Gets the number of bytes taken by this entity.</summary>
public int ByteLength { get; init; }
/// <summary>Gets the size of this entity in pixels.</summary>
public Vector2 Size { get; init; }
/// <summary>Gets the Draw callback.</summary>
public DrawDelegate Draw { get; init; }
/// <summary>Gets a value indicating whether this entity is empty.</summary>
/// <param name="e">Instance of <see cref="SeStringReplacementEntity"/> to test.</param>
public static implicit operator bool(in SeStringReplacementEntity e) => e.ByteLength != 0;
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Data;
@ -6,7 +7,9 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
@ -32,6 +35,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
private ReadOnlySeString? logkind;
private SeStringDrawParams style;
private bool interactable;
private bool useEntity;
/// <inheritdoc/>
public string DisplayName { get; init; } = "SeStringRenderer Test";
@ -45,12 +49,12 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.style = default;
this.style = new() { GetEntity = this.GetEntity };
this.addons = null;
this.uicolor = null;
this.logkind = null;
this.testString = string.Empty;
this.interactable = true;
this.interactable = this.useEntity = true;
this.Ready = true;
}
@ -85,11 +89,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
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;
@ -123,11 +127,15 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
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;
ImGui.SameLine();
t = this.useEntity;
if (ImGui.Checkbox("Use Entity Replacements", ref t))
this.useEntity = t;
if (ImGui.CollapsingHeader("UIColor Preview"))
{
if (this.uicolor is null)
@ -267,7 +275,22 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped(
"· For ease of testing, <colortype(506)><edgecolortype(507)>line breaks<colortype(0)><edgecolortype(0)> are automatically replaced to <colortype(502)><edgecolortype(503)>\\<br><colortype(0)><edgecolortype(0)>.",
"Optional features implemented for the following test input:<br>" +
"· <colortype(506)><edgecolortype(507)>line breaks<colortype(0)><edgecolortype(0)> are automatically replaced to <colortype(502)><edgecolortype(503)>\\<br><colortype(0)><edgecolortype(0)>.<br>" +
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>alamud<colortype(0)><edgecolortype(0)> will display Dalamud.<br>" +
"· <colortype(506)><edgecolortype(507)>W<link(0xCE)>hite<colortype(0)><edgecolortype(0)> will display White.<br>" +
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>efaultIcon<colortype(0)><edgecolortype(0)> will display DefaultIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>isabledIcon<colortype(0)><edgecolortype(0)> will display DisabledIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>O<link(0xCE)>utdatedInstallableIcon<colortype(0)><edgecolortype(0)> will display OutdatedInstallableIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>roubleIcon<colortype(0)><edgecolortype(0)> will display TroubleIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>evPluginIcon<colortype(0)><edgecolortype(0)> will display DevPluginIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>U<link(0xCE)>pdateIcon<colortype(0)><edgecolortype(0)> will display UpdateIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>I<link(0xCE)>nstalledIcon<colortype(0)><edgecolortype(0)> will display InstalledIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>hirdIcon<colortype(0)><edgecolortype(0)> will display ThirdIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>hirdInst<link(0xCE)>alledIcon<colortype(0)><edgecolortype(0)> will display ThirdInstalledIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>C<link(0xCE)>hangelogApiBumpIcon<colortype(0)><edgecolortype(0)> will display ChangelogApiBumpIcon.<br>" +
"· <colortype(506)><edgecolortype(507)>icon<link(0xCE)>(5)<colortype(0)><edgecolortype(0)> will display icon(5). This is different from \\<icon<link(0xCE)>(5)>.<br>" +
"· <colortype(506)><edgecolortype(507)>tex<link(0xCE)>(ui/loadingimage/-nowloading_base25_hr1.tex)<colortype(0)><edgecolortype(0)> will display tex(ui/loadingimage/-nowloading_base25_hr1.tex).",
this.style);
ImGuiHelpers.ScaledDummy(3);
@ -302,10 +325,14 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
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)
{
InteractedPayload: { } payload, InteractedPayloadOffset: var offset,
InteractedPayloadEnvelope: var envelope,
Clicked: var clicked
})
{
ImGui.TextUnformatted($"Hovered[{offset}]: {new ReadOnlySeStringSpan(envelope).ToString()}; {payload}");
if (rr.Clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
if (clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
Util.OpenLink(dlp.ExtraString);
}
}
@ -314,4 +341,138 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style);
}
}
private SeStringReplacementEntity GetEntity(scoped in SeStringDrawState state, int byteOffset)
{
if (!this.useEntity)
return default;
if (state.Span[byteOffset..].StartsWith("Dalamud"u8))
return new(7, new(state.FontSize, state.FontSize), DrawDalamud);
if (state.Span[byteOffset..].StartsWith("White"u8))
return new(5, new(state.FontSize, state.FontSize), DrawWhite);
if (state.Span[byteOffset..].StartsWith("DefaultIcon"u8))
return new(11, new(state.FontSize, state.FontSize), DrawDefaultIcon);
if (state.Span[byteOffset..].StartsWith("DisabledIcon"u8))
return new(12, new(state.FontSize, state.FontSize), DrawDisabledIcon);
if (state.Span[byteOffset..].StartsWith("OutdatedInstallableIcon"u8))
return new(23, new(state.FontSize, state.FontSize), DrawOutdatedInstallableIcon);
if (state.Span[byteOffset..].StartsWith("TroubleIcon"u8))
return new(11, new(state.FontSize, state.FontSize), DrawTroubleIcon);
if (state.Span[byteOffset..].StartsWith("DevPluginIcon"u8))
return new(13, new(state.FontSize, state.FontSize), DrawDevPluginIcon);
if (state.Span[byteOffset..].StartsWith("UpdateIcon"u8))
return new(10, new(state.FontSize, state.FontSize), DrawUpdateIcon);
if (state.Span[byteOffset..].StartsWith("ThirdIcon"u8))
return new(9, new(state.FontSize, state.FontSize), DrawThirdIcon);
if (state.Span[byteOffset..].StartsWith("ThirdInstalledIcon"u8))
return new(18, new(state.FontSize, state.FontSize), DrawThirdInstalledIcon);
if (state.Span[byteOffset..].StartsWith("ChangelogApiBumpIcon"u8))
return new(20, new(state.FontSize, state.FontSize), DrawChangelogApiBumpIcon);
if (state.Span[byteOffset..].StartsWith("InstalledIcon"u8))
return new(13, new(state.FontSize, state.FontSize), DrawInstalledIcon);
if (state.Span[byteOffset..].StartsWith("tex("u8))
{
var off = state.Span[byteOffset..].IndexOf((byte)')');
var tex = Service<TextureManager>
.Get()
.Shared
.GetFromGame(Encoding.UTF8.GetString(state.Span[(byteOffset + 4)..(byteOffset + off)]))
.GetWrapOrEmpty();
return new(off + 1, tex.Size * (state.FontSize / tex.Size.Y), DrawTexture);
}
if (state.Span[byteOffset..].StartsWith("icon("u8))
{
var off = state.Span[byteOffset..].IndexOf((byte)')');
if (int.TryParse(state.Span[(byteOffset + 5)..(byteOffset + off)], out var parsed))
{
var tex = Service<TextureManager>
.Get()
.Shared
.GetFromGameIcon(parsed)
.GetWrapOrEmpty();
return new(off + 1, tex.Size * (state.FontSize / tex.Size.Y), DrawIcon);
}
}
return default;
static void DrawTexture(scoped in SeStringDrawState state, int byteOffset, Vector2 offset)
{
var off = state.Span[byteOffset..].IndexOf((byte)')');
var tex = Service<TextureManager>
.Get()
.Shared
.GetFromGame(Encoding.UTF8.GetString(state.Span[(byteOffset + 4)..(byteOffset + off)]))
.GetWrapOrEmpty();
state.Draw(
tex.ImGuiHandle,
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
tex.Size * (state.FontSize / tex.Size.Y),
Vector2.Zero,
Vector2.One);
}
static void DrawIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset)
{
var off = state.Span[byteOffset..].IndexOf((byte)')');
if (!int.TryParse(state.Span[(byteOffset + 5)..(byteOffset + off)], out var parsed))
return;
var tex = Service<TextureManager>
.Get()
.Shared
.GetFromGameIcon(parsed)
.GetWrapOrEmpty();
state.Draw(
tex.ImGuiHandle,
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
tex.Size * (state.FontSize / tex.Size.Y),
Vector2.Zero,
Vector2.One);
}
static void DrawAsset(scoped in SeStringDrawState state, Vector2 offset, DalamudAsset asset) =>
state.Draw(
Service<DalamudAssetManager>.Get().GetDalamudTextureWrap(asset).ImGuiHandle,
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
new(state.FontSize, state.FontSize),
Vector2.Zero,
Vector2.One);
static void DrawDalamud(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.LogoSmall);
static void DrawWhite(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.White4X4);
static void DrawDefaultIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.DefaultIcon);
static void DrawDisabledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.DisabledIcon);
static void DrawOutdatedInstallableIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.OutdatedInstallableIcon);
static void DrawTroubleIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.TroubleIcon);
static void DrawDevPluginIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.DevPluginIcon);
static void DrawUpdateIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.UpdateIcon);
static void DrawInstalledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.InstalledIcon);
static void DrawThirdIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.ThirdIcon);
static void DrawThirdInstalledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.ThirdInstalledIcon);
static void DrawChangelogApiBumpIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
DrawAsset(state, offset, DalamudAsset.ChangelogApiBumpIcon);
}
}