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.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
namespace Dalamud.Interface; namespace Dalamud.Interface;
@ -247,6 +248,14 @@ public static class ColorHelpers
public static uint Desaturate(uint color, float amount) public static uint Desaturate(uint color, float amount)
=> RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), 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> /// <summary>
/// Fade a color. /// Fade a color.
/// </summary> /// </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> /// <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); 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

@ -1,5 +1,4 @@
using System.Buffers; using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -18,12 +17,10 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using ImGuiNET; using ImGuiNET;
using Lumina.Excel.GeneratedSheets2; using Lumina.Excel.GeneratedSheets2;
using Lumina.Text.Expressions;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -37,13 +34,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class SeStringRenderer : IInternalDisposableService 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 ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118; private const int ImGuiWindowDcOffset = 0x118;
private const int ImGuiWindowTempDataCurrLineTextBaseOffset = 0x38; private const int ImGuiWindowTempDataCurrLineTextBaseOffset = 0x38;
@ -83,29 +73,13 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Parsed <c>gfdata.gfd</c> file, containing bitmap font icon lookup table.</summary> /// <summary>Parsed <c>gfdata.gfd</c> file, containing bitmap font icon lookup table.</summary>
private readonly GfdFile gfd; private readonly GfdFile gfd;
/// <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>Parsed text fragments from a SeString.</summary> /// <summary>Parsed text fragments from a SeString.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<TextFragment> fragments = []; private readonly List<TextFragment> fragments = [];
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary> /// <summary>Color stacks to use while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> colorStack = []; private readonly SeStringColorStackSet colorStackSet;
/// <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>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary> /// <summary>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary>
private ImDrawListSplitter* splitter = ImGuiNative.ImDrawListSplitter_ImDrawListSplitter(); private ImDrawListSplitter* splitter = ImGuiNative.ImDrawListSplitter_ImDrawListSplitter();
@ -113,29 +87,8 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner) private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{ {
var uiColor = dm.Excel.GetSheet<UIColor>()!; this.colorStackSet = new(
var maxId = 0; dm.Excel.GetSheet<UIColor>() ?? throw new InvalidOperationException("Failed to access UIColor sheet."));
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<GfdFile>("common/font/gfdata.gfd")!; this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
// SetUnhandledExceptionFilter(who cares); // SetUnhandledExceptionFilter(who cares);
@ -266,19 +219,11 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(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. // This also does argument validation for drawParams. Do it here.
var state = new DrawState(sss, new(drawParams), this.splitter); var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
// Reset and initialize the state. // Reset and initialize the state.
this.fragments.Clear(); this.fragments.Clear();
this.colorStack.Clear(); this.colorStackSet.Initialize(ref state);
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. // Handle cases where ImGui.AlignTextToFramePadding has been called.
var pCurrentWindow = *(nint*)(ImGui.GetCurrentContext() + ImGuiContextCurrentWindowOffset); var pCurrentWindow = *(nint*)(ImGui.GetCurrentContext() + ImGuiContextCurrentWindowOffset);
@ -292,19 +237,22 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// Calculate size. // Calculate size.
var size = Vector2.Zero; var size = Vector2.Zero;
foreach (ref var fragment in fragmentSpan) foreach (ref var fragment in fragmentSpan)
size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.Params.LineHeight)); size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.LineHeight));
// If we're not drawing at all, stop further processing. // If we're not drawing at all, stop further processing.
if (state.Params.DrawList is null) if (state.DrawList.NativePtr is null)
return new() { Size = size }; return new() { Size = size };
ImGuiNative.ImDrawListSplitter_Split(state.Splitter, state.Params.DrawList, ChannelCount); state.SplitDrawList();
// Draw all text fragments. // Draw all text fragments.
var lastRune = default(Rune); var lastRune = default(Rune);
foreach (ref var f in fragmentSpan) foreach (ref var f in fragmentSpan)
{ {
var data = state.Raw.Data[f.From..f.To]; var data = state.Span[f.From..f.To];
if (f.Entity)
f.Entity.Draw(state, f.From, f.Offset);
else
this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link); this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link);
lastRune = f.LastRune; lastRune = f.LastRune;
} }
@ -326,7 +274,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
continue; continue;
var pos = ImGui.GetMousePos() - state.ScreenOffset - f.Offset; var pos = ImGui.GetMousePos() - state.ScreenOffset - f.Offset;
var sz = new Vector2(f.AdvanceWidth, state.Params.LineHeight); var sz = new Vector2(f.AdvanceWidth, state.LineHeight);
if (pos is { X: >= 0, Y: >= 0 } && pos.X <= sz.X && pos.Y <= sz.Y) if (pos is { X: >= 0, Y: >= 0 } && pos.X <= sz.X && pos.Y <= sz.Y)
{ {
invisibleButtonDrawn = true; invisibleButtonDrawn = true;
@ -357,26 +305,24 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// If any link is being interacted, draw rectangles behind the relevant text fragments. // If any link is being interacted, draw rectangles behind the relevant text fragments.
if (hoveredLinkOffset != -1 || activeLinkOffset != -1) if (hoveredLinkOffset != -1 || activeLinkOffset != -1)
{ {
state.SetCurrentChannel(ChannelLinkBackground); state.SetCurrentChannel(SeStringDrawChannel.Background);
var color = activeLinkOffset == -1 ? state.Params.LinkHoverBackColor : state.Params.LinkActiveBackColor; var color = activeLinkOffset == -1 ? state.LinkHoverBackColor : state.LinkActiveBackColor;
color = ApplyOpacityValue(color, state.Params.Opacity); color = ColorHelpers.ApplyOpacity(color, state.Opacity);
foreach (ref readonly var fragment in fragmentSpan) foreach (ref readonly var fragment in fragmentSpan)
{ {
if (fragment.Link != hoveredLinkOffset && hoveredLinkOffset != -1) if (fragment.Link != hoveredLinkOffset && hoveredLinkOffset != -1)
continue; continue;
if (fragment.Link != activeLinkOffset && activeLinkOffset != -1) if (fragment.Link != activeLinkOffset && activeLinkOffset != -1)
continue; continue;
ImGuiNative.ImDrawList_AddRectFilled( var offset = state.ScreenOffset + fragment.Offset;
state.Params.DrawList, state.DrawList.AddRectFilled(
state.ScreenOffset + fragment.Offset, offset,
state.ScreenOffset + fragment.Offset + new Vector2(fragment.AdvanceWidth, state.Params.LineHeight), offset + new Vector2(fragment.AdvanceWidth, state.LineHeight),
color, color);
0,
ImDrawFlags.None);
} }
} }
ImGuiNative.ImDrawListSplitter_Merge(state.Splitter, state.Params.DrawList); state.MergeDrawList();
var payloadEnumerator = new ReadOnlySeStringSpan( var payloadEnumerator = new ReadOnlySeStringSpan(
hoveredLinkOffset == -1 ? ReadOnlySpan<byte>.Empty : sss.Data[hoveredLinkOffset..]).GetEnumerator(); hoveredLinkOffset == -1 ? ReadOnlySpan<byte>.Empty : sss.Data[hoveredLinkOffset..]).GetEnumerator();
@ -412,18 +358,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0; return displayRune.Value != 0;
} }
/// <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>
private static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
/// <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>
private static uint ApplyOpacityValue(uint rgba, float opacity) =>
((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
private void ReleaseUnmanagedResources() private void ReleaseUnmanagedResources()
{ {
if (this.splitter is not null) if (this.splitter is not null)
@ -437,25 +371,74 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <param name="state">Draw state.</param> /// <param name="state">Draw state.</param>
/// <param name="baseY">Y offset adjustment for all text fragments. Used to honor /// <param name="baseY">Y offset adjustment for all text fragments. Used to honor
/// <see cref="ImGui.AlignTextToFramePadding"/>.</param> /// <see cref="ImGui.AlignTextToFramePadding"/>.</param>
private void CreateTextFragments(ref DrawState state, float baseY) private void CreateTextFragments(ref SeStringDrawState state, float baseY)
{ {
var prev = 0; var prev = 0;
var xy = new Vector2(0, baseY); var xy = new Vector2(0, baseY);
var w = 0f; var w = 0f;
var linkOffset = -1; var link = -1;
foreach (var (breakAt, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString)) foreach (var (breakAt, mandatory) in new LineBreakEnumerator(state.Span, UtfEnumeratorFlags.Utf8SeString))
{ {
var nextLinkOffset = linkOffset; // Might have happened if custom entity was longer than the previous break unit.
if (prev > breakAt)
continue;
var nextLink = link;
for (var first = true; prev < breakAt; first = false) for (var first = true; prev < breakAt; first = false)
{ {
var curr = breakAt; var curr = breakAt;
var entity = default(SeStringReplacementEntity);
// Try to split by link payloads. // Try to split by link payloads and custom entities.
foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..breakAt]).GetOffsetEnumerator()) foreach (var p in new ReadOnlySeStringSpan(state.Span[prev..breakAt]).GetOffsetEnumerator())
{ {
if (p.Payload.MacroCode == MacroCode.Link) var break2 = false;
switch (p.Payload.Type)
{ {
nextLinkOffset = case ReadOnlySePayloadType.Text when state.GetEntity is { } getEntity:
foreach (var oe in UtfEnumerator.From(p.Payload.Body, UtfEnumeratorFlags.Utf8))
{
var entityOffset = prev + p.Offset + oe.ByteOffset;
entity = getEntity(state, entityOffset);
if (!entity)
continue;
if (prev == entityOffset)
{
curr = entityOffset + entity.ByteLength;
}
else
{
entity = default;
curr = entityOffset;
}
break2 = true;
break;
}
break;
case ReadOnlySePayloadType.Macro when
state.GetEntity is { } getEntity &&
getEntity(state, prev + p.Offset) is { ByteLength: > 0 } entity1:
entity = entity1;
if (p.Offset == 0)
{
curr = prev + p.Offset + entity.ByteLength;
}
else
{
entity = default;
curr = prev + p.Offset;
}
break2 = true;
break;
case ReadOnlySePayloadType.Macro when p.Payload.MacroCode == MacroCode.Link:
{
nextLink =
p.Payload.TryGetExpression(out var e) && p.Payload.TryGetExpression(out var e) &&
e.TryGetUInt(out var u) && e.TryGetUInt(out var u) &&
u == (uint)LinkMacroPayloadType.Terminator u == (uint)LinkMacroPayloadType.Terminator
@ -466,37 +449,55 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (p.Offset != 0) if (p.Offset != 0)
{ {
curr = prev + p.Offset; curr = prev + p.Offset;
break2 = true;
break; break;
} }
linkOffset = nextLinkOffset; link = nextLink;
break;
} }
case ReadOnlySePayloadType.Invalid:
default:
break;
}
if (break2) break;
} }
// Create a text fragment without applying wrap width limits for testing. // Create a text fragment without applying wrap width limits for testing.
var fragment = state.CreateFragment(this, prev, curr, curr == breakAt && mandatory, xy, linkOffset); var fragment = this.CreateFragment(state, prev, curr, curr == breakAt && mandatory, xy, link, entity);
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.Params.WrapWidth; var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
// Test if the fragment does not fit into the current line and the current line is not empty, // 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 (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
if (first && xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows) {
// Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity.
if (first || entity)
{ {
// The break unit as a whole does not fit into the current line. Advance to the next line. // The break unit as a whole does not fit into the current line. Advance to the next line.
xy.X = 0; xy.X = 0;
xy.Y += state.Params.LineHeight; xy.Y += state.LineHeight;
w = 0; w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true; CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
fragment.Offset = xy; fragment.Offset = xy;
// Now that the fragment is given its own line, test if it overflows again. // Now that the fragment is given its own line, test if it overflows again.
overflows = fragment.VisibleWidth > state.Params.WrapWidth; overflows = fragment.VisibleWidth > state.WrapWidth;
}
} }
if (overflows) if (overflows)
{
// A replacement entity may not be broken down further.
if (!entity)
{ {
// Create a fragment again that fits into the given width limit. // Create a fragment again that fits into the given width limit.
var remainingWidth = state.Params.WrapWidth - xy.X; var remainingWidth = state.WrapWidth - xy.X;
fragment = state.CreateFragment(this, prev, curr, true, xy, linkOffset, remainingWidth); fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
}
} }
else if (this.fragments.Count > 0 && xy.X != 0) else if (this.fragments.Count > 0 && xy.X != 0)
{ {
@ -507,13 +508,13 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth; xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
// Adjust this fragment's offset from kerning distance. // Adjust this fragment's offset from kerning distance.
xy.X += state.CalculateDistance(this.fragments[^1].LastRune, fragment.FirstRune); xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune);
fragment.Offset = xy; fragment.Offset = xy;
} }
// If the fragment was not broken by wrap width, update the link payload offset. // If the fragment was not broken by wrap width, update the link payload offset.
if (fragment.To == curr) if (fragment.To == curr)
linkOffset = nextLinkOffset; link = nextLink;
w = Math.Max(w, xy.X + fragment.VisibleWidth); w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth; xy.X += fragment.AdvanceWidth;
@ -523,7 +524,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (fragment.BreakAfter) if (fragment.BreakAfter)
{ {
xy.X = w = 0; xy.X = w = 0;
xy.Y += state.Params.LineHeight; xy.Y += state.LineHeight;
} }
} }
} }
@ -537,9 +538,9 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <param name="span">Byte span of the SeString fragment to draw.</param> /// <param name="span">Byte span of the SeString fragment to draw.</param>
/// <param name="lastRune">Rune that preceded this text fragment in the same line, or <c>0</c> if none.</param> /// <param name="lastRune">Rune that preceded this text fragment in the same line, or <c>0</c> if none.</param>
/// <param name="link">Byte offset of the link payload that decorates this text fragment in /// <param name="link">Byte offset of the link payload that decorates this text fragment in
/// <see cref="DrawState.Raw"/>, or <c>-1</c> if none.</param> /// <see cref="SeStringDrawState.Span"/>, or <c>-1</c> if none.</param>
private void DrawTextFragment( private void DrawTextFragment(
ref DrawState state, ref SeStringDrawState state,
Vector2 offset, Vector2 offset,
bool displaySoftHyphen, bool displaySoftHyphen,
ReadOnlySpan<byte> span, ReadOnlySpan<byte> span,
@ -559,186 +560,47 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (!enu.MoveNext()) if (!enu.MoveNext())
continue; continue;
var payload = enu.Current.Payload; if (state.HandleStyleAdjustingPayloads(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; continue;
var size = state.CalculateGfdEntrySize(gfdEntry, out var useHq); if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is var icon and not None &&
state.SetCurrentChannel(ChannelFore); this.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
!gfdEntry.IsEmpty)
{
var size = gfdEntry.CalculateScaledSize(state.FontSize, out var useHq);
state.SetCurrentChannel(SeStringDrawChannel.Foreground);
state.Draw( state.Draw(
offset + new Vector2(x, MathF.Round((state.Params.LineHeight - size.Y) / 2)),
gfdTextureSrv, gfdTextureSrv,
Vector2.Zero, offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size, size,
Vector2.Zero,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0, useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1, useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
ApplyOpacityValue(uint.MaxValue, state.Params.Opacity)); ColorHelpers.ApplyOpacity(uint.MaxValue, state.Opacity));
if (link != -1) if (link != -1)
state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X); state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X);
width = Math.Max(width, x + size.X); width = Math.Max(width, x + size.X);
x += MathF.Round(size.X); x += MathF.Round(size.X);
lastRune = default; lastRune = default;
continue;
} }
default:
continue; continue;
} }
}
if (!TryGetDisplayRune(c.EffectiveRune, out var rune, displaySoftHyphen)) if (!TryGetDisplayRune(c.EffectiveRune, out var rune, displaySoftHyphen))
continue; continue;
ref var g = ref state.FindGlyph(ref rune); ref var g = ref state.FindGlyph(ref rune);
var dist = state.CalculateDistance(lastRune, rune); var dist = state.CalculateScaledDistance(lastRune, rune);
var advanceWidth = MathF.Round(g.AdvanceX * state.FontSizeScale);
lastRune = rune; lastRune = rune;
var dxBold = state.Params.Bold ? 2 : 1; state.DrawGlyph(g, offset + new Vector2(x + dist, 0));
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) if (link != -1)
state.DrawLinkUnderline(offset + new Vector2(x + dist, 0), g.AdvanceX); state.DrawLinkUnderline(offset + new Vector2(x + dist, 0), advanceWidth);
width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale)); width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale));
x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale); x += dist + advanceWidth;
}
return;
static uint TouchColorStack(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];
}
static uint TouchColorTypeStack(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];
} }
} }
@ -831,194 +693,48 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return None; return None;
} }
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
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;
}
/// <summary>Represents a temporary state required for drawing.</summary>
private ref struct DrawState(
ReadOnlySeStringSpan raw,
SeStringDrawParams.Resolved @params,
ImDrawListSplitter* splitter)
{
/// <summary>Raw SeString span.</summary>
public readonly ReadOnlySeStringSpan Raw = raw;
/// <summary>Multiplier value for glyph metrics, so that it scales to <see cref="SeStringDrawParams.FontSize"/>.
/// </summary>
public readonly float FontSizeScale = @params.FontSize / @params.Font->FontSize;
/// <summary>Value obtained from <see cref="ImGui.GetCursorScreenPos"/>.</summary>
public readonly Vector2 ScreenOffset = @params.ScreenOffset;
/// <summary>Splitter to split <see cref="SeStringDrawParams.TargetDrawList"/>.</summary>
public readonly ImDrawListSplitter* Splitter = splitter;
/// <summary>Resolved draw parameters from the caller.</summary>
public SeStringDrawParams.Resolved Params = @params;
/// <summary>Calculates the size in pixels of a GFD entry when drawn.</summary>
/// <param name="gfdEntry">GFD entry to determine the size.</param>
/// <param name="useHq">Whether to draw the HQ texture.</param>
/// <returns>Determined size of the GFD entry when drawn.</returns>
public readonly Vector2 CalculateGfdEntrySize(scoped 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);
}
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
/// <param name="channelIndex">Channel to switch to.</param>
public readonly void SetCurrentChannel(int channelIndex) =>
ImGuiNative.ImDrawListSplitter_SetCurrentChannel(
this.Splitter,
this.Params.DrawList,
channelIndex);
/// <summary>Draws a single glyph.</summary>
/// <param name="offset">Offset of the glyph in pixels w.r.t.
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="g">Glyph to draw.</param>
/// <param name="dyItalic">Transformation for <paramref name="g"/> that will push top and bottom pixels to
/// apply faux italicization.</param>
/// <param name="color">Color of the glyph.</param>
public readonly void Draw(
Vector2 offset,
scoped 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<ImFontAtlasTexture>(g.TextureIndex).TexID,
g.XY0 * this.FontSizeScale,
g.XY1 * this.FontSizeScale,
dyItalic * this.FontSizeScale,
g.UV0,
g.UV1,
color);
/// <summary>Draws a single glyph.</summary>
/// <param name="offset">Offset of the glyph in pixels w.r.t.
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="igTextureId">ImGui texture ID to draw from.</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="dyItalic">Transformation for <paramref name="xy0"/> and <paramref name="xy1"/> that will push
/// top and bottom pixels to apply faux italicization.</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.</param>
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);
}
/// <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>
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);
}
}
/// <summary>Creates a text fragment.</summary> /// <summary>Creates a text fragment.</summary>
/// <param name="renderer">Associated renderer.</param> /// <param name="state">Draw state.</param>
/// <param name="from">Starting byte offset (inclusive) in <see cref="Raw"/> that this fragment deals with. /// <param name="from">Starting byte offset (inclusive) in <see cref="SeStringDrawState.Span"/> that this fragment
/// </param> /// deals with.</param>
/// <param name="to">Ending byte offset (exclusive) in <see cref="Raw"/> that this fragment deals with.</param> /// <param name="to">Ending byte offset (exclusive) in <see cref="SeStringDrawState.Span"/> that this fragment deals
/// with.</param>
/// <param name="breakAfter">Whether to break line after this fragment.</param> /// <param name="breakAfter">Whether to break line after this fragment.</param>
/// <param name="offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param> /// <param name="offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="activeLinkOffset">Byte offset of the link payload in <see cref="Raw"/> that decorates this /// <param name="link">Byte offset of the link payload in <see cref="SeStringDrawState.Span"/> that
/// text fragment.</param> /// decorates this text fragment.</param>
/// <param name="entity">Entity to display in place of this fragment.</param>
/// <param name="wrapWidth">Optional wrap width to stop at while creating this text fragment. Note that at least /// <param name="wrapWidth">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 /// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
/// the wrap width.</param> /// the wrap width.</param>
/// <returns>Newly created text fragment.</returns> /// <returns>Newly created text fragment.</returns>
public readonly TextFragment CreateFragment( private TextFragment CreateFragment(
SeStringRenderer renderer, scoped in SeStringDrawState state,
int from, int from,
int to, int to,
bool breakAfter, bool breakAfter,
Vector2 offset, Vector2 offset,
int activeLinkOffset, int link,
SeStringReplacementEntity entity,
float wrapWidth = float.MaxValue) float wrapWidth = float.MaxValue)
{ {
if (entity)
{
return new(
from,
to,
link,
offset,
entity,
entity.Size.X,
entity.Size.X,
entity.Size.X,
false,
false,
default,
default);
}
var x = 0f; var x = 0f;
var w = 0f; var w = 0f;
var visibleWidth = 0f; var visibleWidth = 0f;
@ -1028,19 +744,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
var lastDisplayRune = default(Rune); var lastDisplayRune = default(Rune);
var lastNonSoftHyphenRune = default(Rune); var lastNonSoftHyphenRune = default(Rune);
var endsWithSoftHyphen = false; var endsWithSoftHyphen = false;
foreach (var c in UtfEnumerator.From(this.Raw.Data[from..to], UtfEnumeratorFlags.Utf8SeString)) foreach (var c in UtfEnumerator.From(state.Span[from..to], UtfEnumeratorFlags.Utf8SeString))
{ {
var byteOffset = from + c.ByteOffset; var byteOffset = from + c.ByteOffset;
var isBreakableWhitespace = false; var isBreakableWhitespace = false;
var effectiveRune = c.EffectiveRune; var effectiveRune = c.EffectiveRune;
Rune displayRune; Rune displayRune;
if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } && if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } &&
renderer.GetBitmapFontIconFor(this.Raw.Data[byteOffset..]) is var icon and not None && this.GetBitmapFontIconFor(state.Span[byteOffset..]) is var icon and not None &&
renderer.gfd.TryGetEntry((uint)icon, out var gfdEntry) && this.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
!gfdEntry.IsEmpty) !gfdEntry.IsEmpty)
{ {
// This is an icon payload. // This is an icon payload.
var size = this.CalculateGfdEntrySize(gfdEntry, out _); var size = gfdEntry.CalculateScaledSize(state.FontSize, out _);
w = Math.Max(w, x + size.X); w = Math.Max(w, x + size.X);
x += MathF.Round(size.X); x += MathF.Round(size.X);
displayRune = default; displayRune = default;
@ -1048,10 +764,10 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
else if (TryGetDisplayRune(effectiveRune, out displayRune)) else if (TryGetDisplayRune(effectiveRune, out displayRune))
{ {
// This is a printable character, or a standard whitespace character. // This is a printable character, or a standard whitespace character.
ref var g = ref this.FindGlyph(ref displayRune); ref var g = ref state.FindGlyph(ref displayRune);
var dist = this.CalculateDistance(lastDisplayRune, displayRune); var dist = state.CalculateScaledDistance(lastDisplayRune, displayRune);
w = Math.Max(w, x + ((dist + g.X1) * this.FontSizeScale)); w = Math.Max(w, x + dist + MathF.Round(g.X1 * state.FontSizeScale));
x += MathF.Round((dist + g.AdvanceX) * this.FontSizeScale); x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
isBreakableWhitespace = isBreakableWhitespace =
Rune.IsWhiteSpace(displayRune) && Rune.IsWhiteSpace(displayRune) &&
@ -1091,8 +807,9 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return new( return new(
from, from,
to, to,
activeLinkOffset, link,
offset, offset,
entity,
visibleWidth, visibleWidth,
advanceWidth, advanceWidth,
advanceWidthWithoutSoftHyphen, advanceWidthWithoutSoftHyphen,
@ -1102,52 +819,37 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
lastNonSoftHyphenRune); lastNonSoftHyphenRune);
} }
/// <summary>Gets the glyph corresponding to the given codepoint.</summary> /// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="rune">An instance of <see cref="Rune"/> that represents a character to display.</param> /// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <returns>Corresponding glyph, or glyph of a fallback character specified from /// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <see cref="ImFont.FallbackChar"/>.</returns> /// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune) /// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
private record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{ {
var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
? ImGuiNative.ImFont_FindGlyph(this.Params.Font, (ushort)rune.Value)
: this.Params.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>
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;
}
/// <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>
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);
}
} }
} }

View file

@ -12,9 +12,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
internal ref struct LineBreakEnumerator internal ref struct LineBreakEnumerator
{ {
private readonly UtfEnumeratorFlags enumeratorFlags; private readonly UtfEnumeratorFlags enumeratorFlags;
private readonly int dataLength;
private UtfEnumerator enumerator; private UtfEnumerator enumerator;
private int dataLength;
private int currentByteOffsetDelta;
private Entry class1; private Entry class1;
private Entry class2; private Entry class2;
@ -24,8 +26,6 @@ internal ref struct LineBreakEnumerator
private int consecutiveRegionalIndicators; private int consecutiveRegionalIndicators;
private bool finished;
/// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary> /// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary>
/// <param name="data">UTF-N byte sequence.</param> /// <param name="data">UTF-N byte sequence.</param>
/// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param> /// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param>
@ -58,11 +58,25 @@ internal ref struct LineBreakEnumerator
/// <inheritdoc cref="IEnumerator{T}.Current"/> /// <inheritdoc cref="IEnumerator{T}.Current"/>
public (int ByteOffset, bool Mandatory) Current { get; private set; } 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"/> /// <inheritdoc cref="IEnumerator.MoveNext"/>
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")] [SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
public bool MoveNext() public bool MoveNext()
{ {
if (this.finished) if (this.Finished)
return false; return false;
while (this.enumerator.MoveNext()) while (this.enumerator.MoveNext())
@ -77,10 +91,10 @@ internal ref struct LineBreakEnumerator
switch (this.HandleCharacter(effectiveInt)) switch (this.HandleCharacter(effectiveInt))
{ {
case LineBreakMode.Mandatory: case LineBreakMode.Mandatory:
this.Current = (this.enumerator.Current.ByteOffset, true); this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, true);
return true; return true;
case LineBreakMode.Optional: case LineBreakMode.Optional:
this.Current = (this.enumerator.Current.ByteOffset, false); this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, false);
return true; return true;
case LineBreakMode.Prohibited: case LineBreakMode.Prohibited:
default: default:
@ -90,8 +104,8 @@ internal ref struct LineBreakEnumerator
// Start and end of text: // Start and end of text:
// LB3 Always break at the end of text. // LB3 Always break at the end of text.
this.Current = (this.dataLength, true); this.Current = (this.dataLength + this.currentByteOffsetDelta, true);
this.finished = true; this.Finished = true;
return 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.Numerics;
using System.Runtime.InteropServices;
using ImGuiNET; using ImGuiNET;
@ -18,14 +16,18 @@ public record struct SeStringDrawParams
/// </remarks> /// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; } public ImDrawListPtr? TargetDrawList { get; set; }
/// <summary>Gets or sets the font to use.</summary> /// <summary>Gets or sets the function to be called on every codepoint and payload for the purpose of offering
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value> /// chances to draw something else instead of glyphs or SeString payload entities.</summary>
public ImFontPtr? Font { get; set; } public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
/// <summary>Gets or sets the screen offset of the left top corner.</summary> /// <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> /// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos"/>.</value>
public Vector2? ScreenOffset { get; set; } 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> /// <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>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>.
/// </value> /// </value>
@ -86,83 +88,23 @@ public record struct SeStringDrawParams
public bool Italic { get; set; } public bool Italic { get; set; }
/// <summary>Gets or sets a value indicating whether the text is rendered with edge.</summary> /// <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; } public bool Edge { get; set; }
/// <summary>Gets or sets a value indicating whether the text is rendered with shadow.</summary> /// <summary>Gets or sets a value indicating whether the text is rendered with shadow.</summary>
public bool Shadow { get; set; } 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 (this.Font ?? ImGui.GetFont()) is var f && f.NativePtr is not null
? f.NativePtr ? f.NativePtr
: throw new ArgumentException("Specified font is empty."); : 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>Gets the effective opacity.</summary>
internal 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;
}
} }

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.Linq;
using System.Numerics;
using System.Text; using System.Text;
using Dalamud.Data; using Dalamud.Data;
@ -6,7 +7,9 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
@ -32,6 +35,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
private ReadOnlySeString? logkind; private ReadOnlySeString? logkind;
private SeStringDrawParams style; private SeStringDrawParams style;
private bool interactable; private bool interactable;
private bool useEntity;
/// <inheritdoc/> /// <inheritdoc/>
public string DisplayName { get; init; } = "SeStringRenderer Test"; public string DisplayName { get; init; } = "SeStringRenderer Test";
@ -45,12 +49,12 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Load() public void Load()
{ {
this.style = default; this.style = new() { GetEntity = this.GetEntity };
this.addons = null; this.addons = null;
this.uicolor = null; this.uicolor = null;
this.logkind = null; this.logkind = null;
this.testString = string.Empty; this.testString = string.Empty;
this.interactable = true; this.interactable = this.useEntity = true;
this.Ready = true; this.Ready = true;
} }
@ -123,11 +127,15 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Checkbox("Word Wrap", ref t)) if (ImGui.Checkbox("Word Wrap", ref t))
this.style.WrapWidth = t ? null : float.PositiveInfinity; this.style.WrapWidth = t ? null : float.PositiveInfinity;
ImGui.SameLine();
t = this.interactable; t = this.interactable;
if (ImGui.Checkbox("Interactable", ref t)) if (ImGui.Checkbox("Interactable", ref t))
this.interactable = 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 (ImGui.CollapsingHeader("UIColor Preview"))
{ {
if (this.uicolor is null) if (this.uicolor is null)
@ -267,7 +275,22 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped( 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); this.style);
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
@ -302,10 +325,14 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (this.interactable) if (this.interactable)
{ {
if (ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style, new("this is an ImGui id")) is 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}"); 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); Util.OpenLink(dlp.ExtraString);
} }
} }
@ -314,4 +341,138 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style); 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);
}
} }