diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs
index 318805529..e99d80cd8 100644
--- a/Dalamud/Interface/ColorHelpers.cs
+++ b/Dalamud/Interface/ColorHelpers.cs
@@ -2,6 +2,7 @@ using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
+using System.Runtime.CompilerServices;
namespace Dalamud.Interface;
@@ -247,6 +248,14 @@ public static class ColorHelpers
public static uint Desaturate(uint color, float amount)
=> RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount));
+ /// Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.
+ /// RGBA value to transform.
+ /// Opacity to apply.
+ /// Transformed value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint ApplyOpacity(uint rgba, float opacity) =>
+ ((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
+
///
/// Fade a color.
///
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
index 194d71957..8559cabdf 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/GfdFile.cs
@@ -145,5 +145,16 @@ internal sealed unsafe class GfdFile : FileResource
/// Gets the UV1 of the HQ version of this entry.
public Vector2 HqUv1 => new((this.Left + this.Width) / 256f, (this.Top + this.Height + 170.5f) / 512f);
+
+ /// Calculates the size in pixels of a GFD entry when drawn along with a text.
+ /// Font size in pixels.
+ /// Whether to draw the HQ texture.
+ /// Determined size of the GFD entry when drawn.
+ 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);
+ }
}
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
new file mode 100644
index 000000000..6d7b0a21a
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
@@ -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;
+
+/// Color stacks to use while evaluating a SeString.
+internal sealed class SeStringColorStackSet
+{
+ /// Parsed , containing colors to use with
+ /// .
+ private readonly uint[] colorTypes;
+
+ /// Parsed , containing colors to use with
+ /// .
+ private readonly uint[] edgeColorTypes;
+
+ /// Foreground color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List colorStack = [];
+
+ /// Edge/border color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List edgeColorStack = [];
+
+ /// Shadow color stack while evaluating a SeString for rendering.
+ /// Touched only from the main thread.
+ private readonly List shadowColorStack = [];
+
+ /// Initializes a new instance of the class.
+ /// UIColor sheet.
+ public SeStringColorStackSet(ExcelSheet 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);
+ }
+ }
+
+ /// Gets a value indicating whether at least one color has been pushed to the edge color stack.
+ public bool HasAdditionalEdgeColor { get; private set; }
+
+ /// Resets the colors in the stack.
+ /// Draw state.
+ 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);
+ }
+
+ /// Handles a payload.
+ /// Draw state.
+ /// Payload to handle.
+ internal void HandleColorPayload(scoped ref SeStringDrawState drawState, ReadOnlySePayloadSpan payload) =>
+ drawState.Color = ColorHelpers.ApplyOpacity(AdjustStack(this.colorStack, payload), drawState.Opacity);
+
+ /// Handles a payload.
+ /// Draw state.
+ /// Payload to handle.
+ 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;
+ }
+
+ /// Handles a payload.
+ /// Draw state.
+ /// Payload to handle.
+ internal void HandleShadowColorPayload(
+ scoped ref SeStringDrawState drawState,
+ ReadOnlySePayloadSpan payload) =>
+ drawState.ShadowColor = ColorHelpers.ApplyOpacity(AdjustStack(this.shadowColorStack, payload), drawState.Opacity);
+
+ /// Handles a payload.
+ /// Draw state.
+ /// Payload to handle.
+ internal void HandleColorTypePayload(
+ scoped ref SeStringDrawState drawState,
+ ReadOnlySePayloadSpan payload) =>
+ drawState.Color = ColorHelpers.ApplyOpacity(AdjustStack(this.colorStack, this.colorTypes, payload), drawState.Opacity);
+
+ /// Handles a payload.
+ /// Draw state.
+ /// Payload to handle.
+ 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;
+ }
+
+ /// Swaps red and blue channels of a given color in ARGB(BB GG RR AA) and ABGR(RR GG BB AA).
+ /// Color to process.
+ /// Swapped color.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
+
+ private static unsafe uint AdjustStack(List rgbaStack, ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var expr))
+ return rgbaStack[^1];
+
+ // Color payloads have BGRA values as its parameter. ImGui expects RGBA values.
+ // Opacity component is ignored.
+ if (expr.TryGetPlaceholderExpression(out var p) && p == (int)ExpressionType.StackColor)
+ {
+ // First item in the stack is the color we started to draw with.
+ if (rgbaStack.Count > 1)
+ rgbaStack.RemoveAt(rgbaStack.Count - 1);
+ return rgbaStack[^1];
+ }
+
+ if (expr.TryGetUInt(out var bgra))
+ {
+ rgbaStack.Add(SwapRedBlue(bgra) | 0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ if (expr.TryGetParameterExpression(out var et, out var op) &&
+ et == (int)ExpressionType.GlobalNumber &&
+ op.TryGetInt(out var i) &&
+ RaptureTextModule.Instance() is var rtm &&
+ rtm is not null &&
+ i > 0 && i <= rtm->TextModule.MacroDecoder.GlobalParameters.Count &&
+ rtm->TextModule.MacroDecoder.GlobalParameters[i - 1] is { Type: TextParameterType.Integer } gp)
+ {
+ rgbaStack.Add(SwapRedBlue((uint)gp.IntValue) | 0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ // Fallback value.
+ rgbaStack.Add(0xFF000000u);
+ return rgbaStack[^1];
+ }
+
+ private static uint AdjustStack(List rgbaStack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var expr))
+ return rgbaStack[^1];
+ if (!expr.TryGetUInt(out var colorTypeIndex))
+ return rgbaStack[^1];
+
+ if (colorTypeIndex == 0)
+ {
+ // First item in the stack is the color we started to draw with.
+ if (rgbaStack.Count > 1)
+ rgbaStack.RemoveAt(rgbaStack.Count - 1);
+ return rgbaStack[^1];
+ }
+
+ // Opacity component is ignored.
+ rgbaStack.Add((colorTypeIndex < colorTypes.Length ? colorTypes[colorTypeIndex] : 0u) | 0xFF000000u);
+
+ return rgbaStack[^1];
+ }
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index b942ef844..23b672a3b 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -1,5 +1,4 @@
using System.Buffers;
-using System.Buffers.Binary;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
@@ -18,12 +17,10 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
-using FFXIVClientStructs.FFXIV.Component.Text;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets2;
-using Lumina.Text.Expressions;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@@ -37,13 +34,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
[ServiceManager.EarlyLoadedService]
internal unsafe class SeStringRenderer : IInternalDisposableService
{
- private const int ChannelLinkBackground = 0;
- private const int ChannelShadow = 1;
- private const int ChannelLinkUnderline = 2;
- private const int ChannelEdge = 3;
- private const int ChannelFore = 4;
- private const int ChannelCount = 5;
-
private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118;
private const int ImGuiWindowTempDataCurrLineTextBaseOffset = 0x38;
@@ -83,29 +73,13 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// Parsed gfdata.gfd file, containing bitmap font icon lookup table.
private readonly GfdFile gfd;
- /// Parsed , containing colors to use with
- /// .
- private readonly uint[] colorTypes;
-
- /// Parsed , containing colors to use with
- /// .
- private readonly uint[] edgeColorTypes;
-
/// Parsed text fragments from a SeString.
/// Touched only from the main thread.
private readonly List fragments = [];
- /// Foreground color stack while evaluating a SeString for rendering.
+ /// Color stacks to use while evaluating a SeString for rendering.
/// Touched only from the main thread.
- private readonly List colorStack = [];
-
- /// Edge/border color stack while evaluating a SeString for rendering.
- /// Touched only from the main thread.
- private readonly List edgeColorStack = [];
-
- /// Shadow color stack while evaluating a SeString for rendering.
- /// Touched only from the main thread.
- private readonly List shadowColorStack = [];
+ private readonly SeStringColorStackSet colorStackSet;
/// Splits a draw list so that different layers of a single glyph can be drawn out of order.
private ImDrawListSplitter* splitter = ImGuiNative.ImDrawListSplitter_ImDrawListSplitter();
@@ -113,29 +87,8 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
[ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{
- var uiColor = dm.Excel.GetSheet()!;
- var maxId = 0;
- foreach (var row in uiColor)
- maxId = (int)Math.Max(row.RowId, maxId);
-
- this.colorTypes = new uint[maxId + 1];
- this.edgeColorTypes = new uint[maxId + 1];
- foreach (var row in uiColor)
- {
- // Contains ABGR.
- this.colorTypes[row.RowId] = row.UIForeground;
- this.edgeColorTypes[row.RowId] = row.UIGlow;
- }
-
- if (BitConverter.IsLittleEndian)
- {
- // ImGui wants RGBA in LE.
- foreach (ref var r in this.colorTypes.AsSpan())
- r = BinaryPrimitives.ReverseEndianness(r);
- foreach (ref var r in this.edgeColorTypes.AsSpan())
- r = BinaryPrimitives.ReverseEndianness(r);
- }
-
+ this.colorStackSet = new(
+ dm.Excel.GetSheet() ?? throw new InvalidOperationException("Failed to access UIColor sheet."));
this.gfd = dm.GetFile("common/font/gfdata.gfd")!;
// 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));
// 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.
this.fragments.Clear();
- this.colorStack.Clear();
- this.edgeColorStack.Clear();
- this.shadowColorStack.Clear();
- this.colorStack.Add(state.Params.Color);
- this.edgeColorStack.Add(state.Params.EdgeColor);
- this.shadowColorStack.Add(state.Params.ShadowColor);
- state.Params.Color = ApplyOpacityValue(state.Params.Color, state.Params.Opacity);
- state.Params.EdgeColor = ApplyOpacityValue(state.Params.EdgeColor, state.Params.EdgeOpacity);
- state.Params.ShadowColor = ApplyOpacityValue(state.Params.ShadowColor, state.Params.Opacity);
+ this.colorStackSet.Initialize(ref state);
// Handle cases where ImGui.AlignTextToFramePadding has been called.
var pCurrentWindow = *(nint*)(ImGui.GetCurrentContext() + ImGuiContextCurrentWindowOffset);
@@ -292,20 +237,23 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// Calculate size.
var size = Vector2.Zero;
foreach (ref var fragment in fragmentSpan)
- size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.Params.LineHeight));
+ size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.LineHeight));
// 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 };
- ImGuiNative.ImDrawListSplitter_Split(state.Splitter, state.Params.DrawList, ChannelCount);
+ state.SplitDrawList();
// Draw all text fragments.
var lastRune = default(Rune);
foreach (ref var f in fragmentSpan)
{
- var data = state.Raw.Data[f.From..f.To];
- this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link);
+ 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);
lastRune = f.LastRune;
}
@@ -326,7 +274,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
continue;
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)
{
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 (hoveredLinkOffset != -1 || activeLinkOffset != -1)
{
- state.SetCurrentChannel(ChannelLinkBackground);
- var color = activeLinkOffset == -1 ? state.Params.LinkHoverBackColor : state.Params.LinkActiveBackColor;
- color = ApplyOpacityValue(color, state.Params.Opacity);
+ state.SetCurrentChannel(SeStringDrawChannel.Background);
+ var color = activeLinkOffset == -1 ? state.LinkHoverBackColor : state.LinkActiveBackColor;
+ color = ColorHelpers.ApplyOpacity(color, state.Opacity);
foreach (ref readonly var fragment in fragmentSpan)
{
if (fragment.Link != hoveredLinkOffset && hoveredLinkOffset != -1)
continue;
if (fragment.Link != activeLinkOffset && activeLinkOffset != -1)
continue;
- ImGuiNative.ImDrawList_AddRectFilled(
- state.Params.DrawList,
- state.ScreenOffset + fragment.Offset,
- state.ScreenOffset + fragment.Offset + new Vector2(fragment.AdvanceWidth, state.Params.LineHeight),
- color,
- 0,
- ImDrawFlags.None);
+ var offset = state.ScreenOffset + fragment.Offset;
+ state.DrawList.AddRectFilled(
+ offset,
+ offset + new Vector2(fragment.AdvanceWidth, state.LineHeight),
+ color);
}
}
- ImGuiNative.ImDrawListSplitter_Merge(state.Splitter, state.Params.DrawList);
+ state.MergeDrawList();
var payloadEnumerator = new ReadOnlySeStringSpan(
hoveredLinkOffset == -1 ? ReadOnlySpan.Empty : sss.Data[hoveredLinkOffset..]).GetEnumerator();
@@ -412,18 +358,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0;
}
- /// Swaps red and blue channels of a given color in ARGB(BB GG RR AA) and ABGR(RR GG BB AA).
- /// Color to process.
- /// Swapped color.
- private static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
-
- /// Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.
- /// RGBA value to transform.
- /// Opacity to apply.
- /// Transformed value.
- private static uint ApplyOpacityValue(uint rgba, float opacity) =>
- ((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
-
private void ReleaseUnmanagedResources()
{
if (this.splitter is not null)
@@ -437,66 +371,133 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// Draw state.
/// Y offset adjustment for all text fragments. Used to honor
/// .
- private void CreateTextFragments(ref DrawState state, float baseY)
+ private void CreateTextFragments(ref SeStringDrawState state, float baseY)
{
var prev = 0;
var xy = new Vector2(0, baseY);
var w = 0f;
- var linkOffset = -1;
- foreach (var (breakAt, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString))
+ var link = -1;
+ 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)
{
var curr = breakAt;
+ var entity = default(SeStringReplacementEntity);
- // Try to split by link payloads.
- foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..breakAt]).GetOffsetEnumerator())
+ // Try to split by link payloads and custom entities.
+ 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 =
- p.Payload.TryGetExpression(out var e) &&
- e.TryGetUInt(out var u) &&
- u == (uint)LinkMacroPayloadType.Terminator
- ? -1
- : prev + p.Offset;
+ 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;
- // Split only if we're not splitting at the beginning.
- if (p.Offset != 0)
+ 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:
{
- curr = prev + p.Offset;
+ nextLink =
+ p.Payload.TryGetExpression(out var e) &&
+ e.TryGetUInt(out var u) &&
+ u == (uint)LinkMacroPayloadType.Terminator
+ ? -1
+ : prev + p.Offset;
+
+ // Split only if we're not splitting at the beginning.
+ if (p.Offset != 0)
+ {
+ curr = prev + p.Offset;
+ break2 = true;
+ break;
+ }
+
+ link = nextLink;
+
break;
}
- linkOffset = nextLinkOffset;
+ case ReadOnlySePayloadType.Invalid:
+ default:
+ break;
}
+
+ if (break2) break;
}
// Create a text fragment without applying wrap width limits for testing.
- var fragment = state.CreateFragment(this, prev, curr, curr == breakAt && mandatory, xy, linkOffset);
- var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.Params.WrapWidth;
+ var fragment = this.CreateFragment(state, prev, curr, curr == breakAt && mandatory, xy, link, entity);
+ 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,
- // if this is the first time testing the current break unit.
- if (first && xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
+ // Test if the fragment does not fit into the current line and the current line is not empty.
+ if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
{
- // The break unit as a whole does not fit into the current line. Advance to the next line.
- xy.X = 0;
- xy.Y += state.Params.LineHeight;
- w = 0;
- CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
- fragment.Offset = xy;
+ // 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.
+ xy.X = 0;
+ xy.Y += state.LineHeight;
+ w = 0;
+ CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
+ fragment.Offset = xy;
- // Now that the fragment is given its own line, test if it overflows again.
- overflows = fragment.VisibleWidth > state.Params.WrapWidth;
+ // Now that the fragment is given its own line, test if it overflows again.
+ overflows = fragment.VisibleWidth > state.WrapWidth;
+ }
}
if (overflows)
{
- // Create a fragment again that fits into the given width limit.
- var remainingWidth = state.Params.WrapWidth - xy.X;
- fragment = state.CreateFragment(this, prev, curr, true, xy, linkOffset, remainingWidth);
+ // A replacement entity may not be broken down further.
+ if (!entity)
+ {
+ // Create a fragment again that fits into the given width limit.
+ var remainingWidth = state.WrapWidth - xy.X;
+ fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
+ }
}
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;
// 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;
}
// If the fragment was not broken by wrap width, update the link payload offset.
if (fragment.To == curr)
- linkOffset = nextLinkOffset;
+ link = nextLink;
w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth;
@@ -523,7 +524,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (fragment.BreakAfter)
{
xy.X = w = 0;
- xy.Y += state.Params.LineHeight;
+ xy.Y += state.LineHeight;
}
}
}
@@ -537,9 +538,9 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// Byte span of the SeString fragment to draw.
/// Rune that preceded this text fragment in the same line, or 0 if none.
/// Byte offset of the link payload that decorates this text fragment in
- /// , or -1 if none.
+ /// , or -1 if none.
private void DrawTextFragment(
- ref DrawState state,
+ ref SeStringDrawState state,
Vector2 offset,
bool displaySoftHyphen,
ReadOnlySpan span,
@@ -559,186 +560,47 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (!enu.MoveNext())
continue;
- var payload = enu.Current.Payload;
- switch (payload.MacroCode)
+ if (state.HandleStyleAdjustingPayloads(enu.Current.Payload))
+ continue;
+
+ if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is var icon and not None &&
+ this.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
+ !gfdEntry.IsEmpty)
{
- case MacroCode.Color:
- state.Params.Color = ApplyOpacityValue(
- TouchColorStack(this.colorStack, payload),
- state.Params.Opacity);
- continue;
- case MacroCode.EdgeColor:
- state.Params.EdgeColor = TouchColorStack(this.edgeColorStack, payload);
- state.Params.EdgeColor = ApplyOpacityValue(
- state.Params.ForceEdgeColor ? this.edgeColorStack[0] : state.Params.EdgeColor,
- state.Params.EdgeOpacity);
- continue;
- case MacroCode.ShadowColor:
- state.Params.ShadowColor = ApplyOpacityValue(
- TouchColorStack(this.shadowColorStack, payload),
- state.Params.Opacity);
- continue;
- case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
- // doesn't actually work in chat log
- state.Params.Bold = u != 0;
- continue;
- case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
- state.Params.Italic = u != 0;
- continue;
- case MacroCode.Edge when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
- state.Params.Edge = u != 0;
- continue;
- case MacroCode.Shadow when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
- state.Params.Shadow = u != 0;
- continue;
- case MacroCode.ColorType:
- state.Params.Color = ApplyOpacityValue(
- TouchColorTypeStack(this.colorStack, this.colorTypes, payload),
- state.Params.Opacity);
- continue;
- case MacroCode.EdgeColorType:
- state.Params.EdgeColor = TouchColorTypeStack(this.edgeColorStack, this.edgeColorTypes, payload);
- state.Params.EdgeColor = ApplyOpacityValue(
- state.Params.ForceEdgeColor ? this.edgeColorStack[0] : state.Params.EdgeColor,
- state.Params.EdgeOpacity);
- continue;
- case MacroCode.Icon:
- case MacroCode.Icon2:
- {
- if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is not (var icon and not None) ||
- !this.gfd.TryGetEntry((uint)icon, out var gfdEntry) ||
- gfdEntry.IsEmpty)
- continue;
+ var size = gfdEntry.CalculateScaledSize(state.FontSize, out var useHq);
+ state.SetCurrentChannel(SeStringDrawChannel.Foreground);
+ state.Draw(
+ gfdTextureSrv,
+ offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
+ size,
+ useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
+ useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
+ ColorHelpers.ApplyOpacity(uint.MaxValue, state.Opacity));
+ if (link != -1)
+ state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X);
- var size = state.CalculateGfdEntrySize(gfdEntry, out var useHq);
- state.SetCurrentChannel(ChannelFore);
- state.Draw(
- offset + new Vector2(x, MathF.Round((state.Params.LineHeight - size.Y) / 2)),
- gfdTextureSrv,
- Vector2.Zero,
- size,
- Vector2.Zero,
- useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
- useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
- ApplyOpacityValue(uint.MaxValue, state.Params.Opacity));
- if (link != -1)
- state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X);
-
- width = Math.Max(width, x + size.X);
- x += MathF.Round(size.X);
- lastRune = default;
- continue;
- }
-
- default:
- continue;
+ width = Math.Max(width, x + size.X);
+ x += MathF.Round(size.X);
+ lastRune = default;
}
+
+ continue;
}
if (!TryGetDisplayRune(c.EffectiveRune, out var rune, displaySoftHyphen))
continue;
ref var g = ref state.FindGlyph(ref rune);
- var dist = state.CalculateDistance(lastRune, rune);
+ var dist = state.CalculateScaledDistance(lastRune, rune);
+ var advanceWidth = MathF.Round(g.AdvanceX * state.FontSizeScale);
lastRune = rune;
- var dxBold = state.Params.Bold ? 2 : 1;
- var dyItalic = state.Params.Italic
- ? new Vector2(state.Params.FontSize - g.Y0, state.Params.FontSize - g.Y1) / 6
- : Vector2.Zero;
-
- if (state.Params is { Shadow: true, ShadowColor: >= 0x1000000 })
- {
- state.SetCurrentChannel(ChannelShadow);
- for (var dx = 0; dx < dxBold; dx++)
- state.Draw(offset + new Vector2(x + dist + dx, 1), g, dyItalic, state.Params.ShadowColor);
- }
-
- if ((state.Params.Edge || this.edgeColorStack.Count > 1) && state.Params.EdgeColor >= 0x1000000)
- {
- state.SetCurrentChannel(ChannelEdge);
- for (var dx = -1; dx <= dxBold; dx++)
- {
- for (var dy = -1; dy <= 1; dy++)
- {
- if (dx >= 0 && dx < dxBold && dy == 0)
- continue;
-
- state.Draw(offset + new Vector2(x + dist + dx, dy), g, dyItalic, state.Params.EdgeColor);
- }
- }
- }
-
- state.SetCurrentChannel(ChannelFore);
- for (var dx = 0; dx < dxBold; dx++)
- state.Draw(offset + new Vector2(x + dist + dx, 0), g, dyItalic, state.Params.Color);
-
+ state.DrawGlyph(g, offset + new Vector2(x + dist, 0));
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));
- x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
- }
-
- return;
-
- static uint TouchColorStack(List rgbaStack, ReadOnlySePayloadSpan payload)
- {
- if (!payload.TryGetExpression(out var expr))
- return rgbaStack[^1];
-
- // Color payloads have BGRA values as its parameter. ImGui expects RGBA values.
- // Opacity component is ignored.
- if (expr.TryGetPlaceholderExpression(out var p) && p == (int)ExpressionType.StackColor)
- {
- // First item in the stack is the color we started to draw with.
- if (rgbaStack.Count > 1)
- rgbaStack.RemoveAt(rgbaStack.Count - 1);
- return rgbaStack[^1];
- }
-
- if (expr.TryGetUInt(out var bgra))
- {
- rgbaStack.Add(SwapRedBlue(bgra) | 0xFF000000u);
- return rgbaStack[^1];
- }
-
- if (expr.TryGetParameterExpression(out var et, out var op) &&
- et == (int)ExpressionType.GlobalNumber &&
- op.TryGetInt(out var i) &&
- RaptureTextModule.Instance() is var rtm &&
- rtm is not null &&
- i > 0 && i <= rtm->TextModule.MacroDecoder.GlobalParameters.Count &&
- rtm->TextModule.MacroDecoder.GlobalParameters[i - 1] is { Type: TextParameterType.Integer } gp)
- {
- rgbaStack.Add(SwapRedBlue((uint)gp.IntValue) | 0xFF000000u);
- return rgbaStack[^1];
- }
-
- // Fallback value.
- rgbaStack.Add(0xFF000000u);
- return rgbaStack[^1];
- }
-
- static uint TouchColorTypeStack(List rgbaStack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
- {
- if (!payload.TryGetExpression(out var expr))
- return rgbaStack[^1];
- if (!expr.TryGetUInt(out var colorTypeIndex))
- return rgbaStack[^1];
-
- if (colorTypeIndex == 0)
- {
- // First item in the stack is the color we started to draw with.
- if (rgbaStack.Count > 1)
- rgbaStack.RemoveAt(rgbaStack.Count - 1);
- return rgbaStack[^1];
- }
-
- // Opacity component is ignored.
- rgbaStack.Add((colorTypeIndex < colorTypes.Length ? colorTypes[colorTypeIndex] : 0u) | 0xFF000000u);
-
- return rgbaStack[^1];
+ x += dist + advanceWidth;
}
}
@@ -831,11 +693,138 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return None;
}
+ /// Creates a text fragment.
+ /// Draw state.
+ /// Starting byte offset (inclusive) in that this fragment
+ /// deals with.
+ /// Ending byte offset (exclusive) in that this fragment deals
+ /// with.
+ /// Whether to break line after this fragment.
+ /// Offset in pixels w.r.t. .
+ /// Byte offset of the link payload in that
+ /// decorates this text fragment.
+ /// Entity to display in place of this fragment.
+ /// Optional wrap width to stop at while creating this text fragment. Note that at least
+ /// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
+ /// the wrap width.
+ /// Newly created text fragment.
+ private TextFragment CreateFragment(
+ scoped in SeStringDrawState state,
+ int from,
+ int to,
+ bool breakAfter,
+ Vector2 offset,
+ int link,
+ SeStringReplacementEntity entity,
+ 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 w = 0f;
+ var visibleWidth = 0f;
+ var advanceWidth = 0f;
+ var advanceWidthWithoutSoftHyphen = 0f;
+ var firstDisplayRune = default(Rune?);
+ var lastDisplayRune = default(Rune);
+ var lastNonSoftHyphenRune = default(Rune);
+ var endsWithSoftHyphen = false;
+ foreach (var c in UtfEnumerator.From(state.Span[from..to], UtfEnumeratorFlags.Utf8SeString))
+ {
+ var byteOffset = from + c.ByteOffset;
+ var isBreakableWhitespace = false;
+ var effectiveRune = c.EffectiveRune;
+ Rune displayRune;
+ if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } &&
+ this.GetBitmapFontIconFor(state.Span[byteOffset..]) is var icon and not None &&
+ this.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
+ !gfdEntry.IsEmpty)
+ {
+ // This is an icon payload.
+ var size = gfdEntry.CalculateScaledSize(state.FontSize, out _);
+ w = Math.Max(w, x + size.X);
+ x += MathF.Round(size.X);
+ displayRune = default;
+ }
+ else if (TryGetDisplayRune(effectiveRune, out displayRune))
+ {
+ // This is a printable character, or a standard whitespace character.
+ ref var g = ref state.FindGlyph(ref displayRune);
+ var dist = state.CalculateScaledDistance(lastDisplayRune, displayRune);
+ w = Math.Max(w, x + dist + MathF.Round(g.X1 * state.FontSizeScale));
+ x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
+
+ isBreakableWhitespace =
+ Rune.IsWhiteSpace(displayRune) &&
+ UnicodeData.LineBreak[displayRune.Value] is not UnicodeLineBreakClass.GL;
+ }
+ else
+ {
+ continue;
+ }
+
+ if (isBreakableWhitespace)
+ {
+ advanceWidth = x;
+ }
+ else
+ {
+ if (firstDisplayRune is not null && w > wrapWidth && effectiveRune.Value != SoftHyphen)
+ {
+ to = byteOffset;
+ break;
+ }
+
+ advanceWidth = x;
+ visibleWidth = w;
+ }
+
+ firstDisplayRune ??= displayRune;
+ lastDisplayRune = displayRune;
+ endsWithSoftHyphen = effectiveRune.Value == SoftHyphen;
+ if (!endsWithSoftHyphen)
+ {
+ advanceWidthWithoutSoftHyphen = x;
+ lastNonSoftHyphenRune = displayRune;
+ }
+ }
+
+ return new(
+ from,
+ to,
+ link,
+ offset,
+ entity,
+ visibleWidth,
+ advanceWidth,
+ advanceWidthWithoutSoftHyphen,
+ breakAfter,
+ endsWithSoftHyphen,
+ firstDisplayRune ?? default,
+ lastNonSoftHyphenRune);
+ }
+
/// Represents a text fragment in a SeString span.
/// Starting byte offset (inclusive) in a SeString.
/// Ending byte offset (exclusive) in a SeString.
/// Byte offset of the link that decorates this text fragment, or -1 if none.
/// Offset in pixels w.r.t. .
+ /// Replacement entity, if any.
/// Visible width of this text fragment. This is the width required to draw everything
/// without clipping.
/// Advance width of this text fragment. This is the width required to add to the cursor
@@ -852,6 +841,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
int To,
int Link,
Vector2 Offset,
+ SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
@@ -862,292 +852,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
{
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}
-
- /// Represents a temporary state required for drawing.
- private ref struct DrawState(
- ReadOnlySeStringSpan raw,
- SeStringDrawParams.Resolved @params,
- ImDrawListSplitter* splitter)
- {
- /// Raw SeString span.
- public readonly ReadOnlySeStringSpan Raw = raw;
-
- /// Multiplier value for glyph metrics, so that it scales to .
- ///
- public readonly float FontSizeScale = @params.FontSize / @params.Font->FontSize;
-
- /// Value obtained from .
- public readonly Vector2 ScreenOffset = @params.ScreenOffset;
-
- /// Splitter to split .
- public readonly ImDrawListSplitter* Splitter = splitter;
-
- /// Resolved draw parameters from the caller.
- public SeStringDrawParams.Resolved Params = @params;
-
- /// Calculates the size in pixels of a GFD entry when drawn.
- /// GFD entry to determine the size.
- /// Whether to draw the HQ texture.
- /// Determined size of the GFD entry when drawn.
- public readonly Vector2 CalculateGfdEntrySize(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);
- }
-
- /// Sets the current channel in the ImGui draw list splitter.
- /// Channel to switch to.
- public readonly void SetCurrentChannel(int channelIndex) =>
- ImGuiNative.ImDrawListSplitter_SetCurrentChannel(
- this.Splitter,
- this.Params.DrawList,
- channelIndex);
-
- /// Draws a single glyph.
- /// Offset of the glyph in pixels w.r.t.
- /// .
- /// Glyph to draw.
- /// Transformation for that will push top and bottom pixels to
- /// apply faux italicization.
- /// Color of the glyph.
- public readonly void Draw(
- Vector2 offset,
- 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(g.TextureIndex).TexID,
- g.XY0 * this.FontSizeScale,
- g.XY1 * this.FontSizeScale,
- dyItalic * this.FontSizeScale,
- g.UV0,
- g.UV1,
- color);
-
- /// Draws a single glyph.
- /// Offset of the glyph in pixels w.r.t.
- /// .
- /// ImGui texture ID to draw from.
- /// Left top corner of the glyph w.r.t. its glyph origin in the target draw list.
- /// Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.
- /// Transformation for and that will push
- /// top and bottom pixels to apply faux italicization.
- /// Left top corner of the glyph w.r.t. its glyph origin in the source texture.
- /// Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.
- /// Color of the glyph.
- public readonly void Draw(
- Vector2 offset,
- nint igTextureId,
- Vector2 xy0,
- Vector2 xy1,
- Vector2 dyItalic,
- Vector2 uv0,
- Vector2 uv1,
- uint color = uint.MaxValue)
- {
- offset += this.ScreenOffset;
- ImGuiNative.ImDrawList_AddImageQuad(
- this.Params.DrawList,
- igTextureId,
- offset + new Vector2(xy0.X + dyItalic.X, xy0.Y),
- offset + new Vector2(xy0.X + dyItalic.Y, xy1.Y),
- offset + new Vector2(xy1.X + dyItalic.Y, xy1.Y),
- offset + new Vector2(xy1.X + dyItalic.X, xy0.Y),
- new(uv0.X, uv0.Y),
- new(uv0.X, uv1.Y),
- new(uv1.X, uv1.Y),
- new(uv1.X, uv0.Y),
- color);
- }
-
- /// Draws an underline, for links.
- /// Offset of the glyph in pixels w.r.t.
- /// .
- /// Advance width of the glyph.
- public readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
- {
- if (this.Params.LinkUnderlineThickness < 1f)
- return;
-
- var dy = (this.Params.LinkUnderlineThickness - 1) / 2f;
- dy += MathF.Round(
- (((this.Params.LineHeight - this.Params.FontSize) / 2) + this.Params.Font->Ascent) *
- this.FontSizeScale);
- this.SetCurrentChannel(ChannelLinkUnderline);
- ImGuiNative.ImDrawList_AddLine(
- this.Params.DrawList,
- this.ScreenOffset + offset + new Vector2(0, dy),
- this.ScreenOffset + offset + new Vector2(advanceWidth, dy),
- this.Params.Color,
- this.Params.LinkUnderlineThickness);
-
- if (this.Params is { Shadow: true, ShadowColor: >= 0x1000000 })
- {
- this.SetCurrentChannel(ChannelShadow);
- ImGuiNative.ImDrawList_AddLine(
- this.Params.DrawList,
- this.ScreenOffset + offset + new Vector2(0, dy + 1),
- this.ScreenOffset + offset + new Vector2(advanceWidth, dy + 1),
- this.Params.ShadowColor,
- this.Params.LinkUnderlineThickness);
- }
- }
-
- /// Creates a text fragment.
- /// Associated renderer.
- /// Starting byte offset (inclusive) in that this fragment deals with.
- ///
- /// Ending byte offset (exclusive) in that this fragment deals with.
- /// Whether to break line after this fragment.
- /// Offset in pixels w.r.t. .
- /// Byte offset of the link payload in that decorates this
- /// text fragment.
- /// Optional wrap width to stop at while creating this text fragment. Note that at least
- /// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
- /// the wrap width.
- /// Newly created text fragment.
- public readonly TextFragment CreateFragment(
- SeStringRenderer renderer,
- int from,
- int to,
- bool breakAfter,
- Vector2 offset,
- int activeLinkOffset,
- float wrapWidth = float.MaxValue)
- {
- var x = 0f;
- var w = 0f;
- var visibleWidth = 0f;
- var advanceWidth = 0f;
- var advanceWidthWithoutSoftHyphen = 0f;
- var firstDisplayRune = default(Rune?);
- var lastDisplayRune = default(Rune);
- var lastNonSoftHyphenRune = default(Rune);
- var endsWithSoftHyphen = false;
- foreach (var c in UtfEnumerator.From(this.Raw.Data[from..to], UtfEnumeratorFlags.Utf8SeString))
- {
- var byteOffset = from + c.ByteOffset;
- var isBreakableWhitespace = false;
- var effectiveRune = c.EffectiveRune;
- Rune displayRune;
- if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } &&
- renderer.GetBitmapFontIconFor(this.Raw.Data[byteOffset..]) is var icon and not None &&
- renderer.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
- !gfdEntry.IsEmpty)
- {
- // This is an icon payload.
- var size = this.CalculateGfdEntrySize(gfdEntry, out _);
- w = Math.Max(w, x + size.X);
- x += MathF.Round(size.X);
- displayRune = default;
- }
- else if (TryGetDisplayRune(effectiveRune, out displayRune))
- {
- // This is a printable character, or a standard whitespace character.
- ref var g = ref this.FindGlyph(ref displayRune);
- var dist = this.CalculateDistance(lastDisplayRune, displayRune);
- w = Math.Max(w, x + ((dist + g.X1) * this.FontSizeScale));
- x += MathF.Round((dist + g.AdvanceX) * this.FontSizeScale);
-
- isBreakableWhitespace =
- Rune.IsWhiteSpace(displayRune) &&
- UnicodeData.LineBreak[displayRune.Value] is not UnicodeLineBreakClass.GL;
- }
- else
- {
- continue;
- }
-
- if (isBreakableWhitespace)
- {
- advanceWidth = x;
- }
- else
- {
- if (firstDisplayRune is not null && w > wrapWidth && effectiveRune.Value != SoftHyphen)
- {
- to = byteOffset;
- break;
- }
-
- advanceWidth = x;
- visibleWidth = w;
- }
-
- firstDisplayRune ??= displayRune;
- lastDisplayRune = displayRune;
- endsWithSoftHyphen = effectiveRune.Value == SoftHyphen;
- if (!endsWithSoftHyphen)
- {
- advanceWidthWithoutSoftHyphen = x;
- lastNonSoftHyphenRune = displayRune;
- }
- }
-
- return new(
- from,
- to,
- activeLinkOffset,
- offset,
- visibleWidth,
- advanceWidth,
- advanceWidthWithoutSoftHyphen,
- breakAfter,
- endsWithSoftHyphen,
- firstDisplayRune ?? default,
- lastNonSoftHyphenRune);
- }
-
- /// Gets the glyph corresponding to the given codepoint.
- /// An instance of that represents a character to display.
- /// Corresponding glyph, or glyph of a fallback character specified from
- /// .
- public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
- {
- var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
- ? ImGuiNative.ImFont_FindGlyph(this.Params.Font, (ushort)rune.Value)
- : this.Params.Font->FallbackGlyph;
- return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
- }
-
- /// Gets the glyph corresponding to the given codepoint.
- /// An instance of that represents a character to display, that will be
- /// changed on return to the rune corresponding to the fallback glyph if a glyph not corresponding to the
- /// requested glyph is being returned.
- /// Corresponding glyph, or glyph of a fallback character specified from
- /// .
- public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(ref Rune rune)
- {
- ref var glyph = ref this.FindGlyph(rune);
- if (rune.Value != glyph.Codepoint && !Rune.TryCreate(glyph.Codepoint, out rune))
- rune = Rune.ReplacementChar;
- return ref glyph;
- }
-
- /// Gets the kerning adjustment between two glyphs in a succession corresponding to the given runes.
- ///
- /// Rune representing the glyph on the left side of a pair.
- /// Rune representing the glyph on the right side of a pair.
- /// Distance adjustment in pixels, scaled to the size specified from
- /// , and rounded.
- public readonly float CalculateDistance(Rune left, Rune right)
- {
- // Kerning distance entries are ignored if NUL, U+FFFF(invalid Unicode character), or characters outside
- // the basic multilingual plane(BMP) is involved.
- if (left.Value is <= 0 or >= char.MaxValue)
- return 0;
- if (right.Value is <= 0 or >= char.MaxValue)
- return 0;
-
- return MathF.Round(
- ImGuiNative.ImFont_GetDistanceAdjustmentForPair(
- this.Params.Font,
- (ushort)left.Value,
- (ushort)right.Value) * this.FontSizeScale);
- }
- }
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
index 9113ef703..fa994bcd2 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextProcessing/LineBreakEnumerator.cs
@@ -12,9 +12,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
internal ref struct LineBreakEnumerator
{
private readonly UtfEnumeratorFlags enumeratorFlags;
- private readonly int dataLength;
private UtfEnumerator enumerator;
+ private int dataLength;
+ private int currentByteOffsetDelta;
+
private Entry class1;
private Entry class2;
@@ -24,8 +26,6 @@ internal ref struct LineBreakEnumerator
private int consecutiveRegionalIndicators;
- private bool finished;
-
/// Initializes a new instance of the struct.
/// UTF-N byte sequence.
/// Flags to pass to sub-enumerator.
@@ -58,11 +58,25 @@ internal ref struct LineBreakEnumerator
///
public (int ByteOffset, bool Mandatory) Current { get; private set; }
+ /// Gets a value indicating whether the end of the underlying span has been reached.
+ public bool Finished { get; private set; }
+
+ /// Resumes enumeration with the given data.
+ /// The data.
+ /// Offset to add to .ByteOffset.
+ public void ResumeWith(ReadOnlySpan data, int offsetDelta)
+ {
+ this.enumerator = UtfEnumerator.From(data, this.enumeratorFlags);
+ this.dataLength = data.Length;
+ this.currentByteOffsetDelta = offsetDelta;
+ this.Finished = false;
+ }
+
///
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
public bool MoveNext()
{
- if (this.finished)
+ if (this.Finished)
return false;
while (this.enumerator.MoveNext())
@@ -77,10 +91,10 @@ internal ref struct LineBreakEnumerator
switch (this.HandleCharacter(effectiveInt))
{
case LineBreakMode.Mandatory:
- this.Current = (this.enumerator.Current.ByteOffset, true);
+ this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, true);
return true;
case LineBreakMode.Optional:
- this.Current = (this.enumerator.Current.ByteOffset, false);
+ this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, false);
return true;
case LineBreakMode.Prohibited:
default:
@@ -90,8 +104,8 @@ internal ref struct LineBreakEnumerator
// Start and end of text:
// LB3 Always break at the end of text.
- this.Current = (this.dataLength, true);
- this.finished = true;
+ this.Current = (this.dataLength + this.currentByteOffsetDelta, true);
+ this.Finished = true;
return true;
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawChannel.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawChannel.cs
new file mode 100644
index 000000000..d34a9ee5b
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawChannel.cs
@@ -0,0 +1,42 @@
+namespace Dalamud.Interface.ImGuiSeStringRenderer;
+
+/// Predefined channels for drawing onto, for out-of-order drawing.
+// Notes: values must be consecutively increasing, starting from 0. Higher values has higher priority.
+public enum SeStringDrawChannel
+{
+ /// Next draw operation on the draw list will be put below .
+ BelowBackground,
+
+ /// Next draw operation on the draw list will be put onto the background channel.
+ Background,
+
+ /// Next draw operation on the draw list will be put above .
+ AboveBackground,
+
+ /// Next draw operation on the draw list will be put below .
+ BelowShadow,
+
+ /// Next draw operation on the draw list will be put onto the shadow channel.
+ Shadow,
+
+ /// Next draw operation on the draw list will be put above .
+ AboveShadow,
+
+ /// Next draw operation on the draw list will be put below .
+ BelowEdge,
+
+ /// Next draw operation on the draw list will be put onto the edge channel.
+ Edge,
+
+ /// Next draw operation on the draw list will be put above .
+ AboveEdge,
+
+ /// Next draw operation on the draw list will be put below .
+ BelowForeground,
+
+ /// Next draw operation on the draw list will be put onto the foreground channel.
+ Foreground,
+
+ /// Next draw operation on the draw list will be put above .
+ AboveForeground,
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
index 543f4c07a..cdd5e1db6 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -1,6 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
using System.Numerics;
-using System.Runtime.InteropServices;
using ImGuiNET;
@@ -18,14 +16,18 @@ public record struct SeStringDrawParams
///
public ImDrawListPtr? TargetDrawList { get; set; }
- /// Gets or sets the font to use.
- /// Font to use, or null to use (the default).
- public ImFontPtr? Font { get; set; }
+ /// Gets or sets the function to be called on every codepoint and payload for the purpose of offering
+ /// chances to draw something else instead of glyphs or SeString payload entities.
+ public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
/// Gets or sets the screen offset of the left top corner.
/// Screen offset to draw at, or null to use .
public Vector2? ScreenOffset { get; set; }
+ /// Gets or sets the font to use.
+ /// Font to use, or null to use (the default).
+ public ImFontPtr? Font { get; set; }
+
/// Gets or sets the font size.
/// Font size in pixels, or 0 to use the current ImGui font size .
///
@@ -86,83 +88,23 @@ public record struct SeStringDrawParams
public bool Italic { get; set; }
/// Gets or sets a value indicating whether the text is rendered with edge.
+ /// If an edge color is pushed with or
+ /// , it will be drawn regardless. Set to
+ /// true and set to 0 to fully disable edge.
public bool Edge { get; set; }
/// Gets or sets a value indicating whether the text is rendered with shadow.
public bool Shadow { get; set; }
- private readonly unsafe ImFont* EffectiveFont =>
+ /// Gets the effective font.
+ internal readonly unsafe ImFont* EffectiveFont =>
(this.Font ?? ImGui.GetFont()) is var f && f.NativePtr is not null
? f.NativePtr
: throw new ArgumentException("Specified font is empty.");
- private readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
+ /// Gets the effective line height in pixels.
+ internal readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
- private readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
-
- /// Calculated values from using ImGui styles.
- [SuppressMessage(
- "StyleCop.CSharp.OrderingRules",
- "SA1214:Readonly fields should appear before non-readonly fields",
- Justification = "Matching the above order.")]
- [StructLayout(LayoutKind.Sequential)]
- internal unsafe struct Resolved(in SeStringDrawParams ssdp)
- {
- ///
- public readonly ImDrawList* DrawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
-
- ///
- public readonly ImFont* Font = ssdp.EffectiveFont;
-
- ///
- public readonly Vector2 ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
-
- ///
- public readonly float FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
-
- ///
- public readonly float LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
-
- ///
- public readonly float WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
-
- ///
- public readonly float LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
-
- ///
- public readonly float Opacity = ssdp.EffectiveOpacity;
-
- ///
- public readonly float EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
-
- ///
- public uint Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
-
- ///
- public uint EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
-
- ///
- public uint ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
-
- ///
- public readonly uint LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
-
- ///
- public readonly uint LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
-
- ///
- public readonly bool ForceEdgeColor = ssdp.ForceEdgeColor;
-
- ///
- public bool Bold = ssdp.Bold;
-
- ///
- public bool Italic = ssdp.Italic;
-
- ///
- public bool Edge = ssdp.Edge;
-
- ///
- public bool Shadow = ssdp.Shadow;
- }
+ /// Gets the effective opacity.
+ internal readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
new file mode 100644
index 000000000..d8348e5f2
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -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;
+
+/// Calculated values from using ImGui styles.
+[StructLayout(LayoutKind.Sequential)]
+public unsafe ref struct SeStringDrawState
+{
+ private static readonly int ChannelCount = Enum.GetValues().Length;
+
+ private readonly ImDrawList* drawList;
+ private readonly SeStringColorStackSet colorStackSet;
+ private readonly ImDrawListSplitter* splitter;
+
+ /// Initializes a new instance of the struct.
+ /// Raw SeString byte span.
+ /// Instance of to initialize from.
+ /// Instance of to use.
+ /// Instance of ImGui Splitter to use.
+ internal SeStringDrawState(
+ ReadOnlySpan 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;
+ }
+
+ ///
+ public readonly ImDrawListPtr DrawList => new(this.drawList);
+
+ /// Gets the raw SeString byte span.
+ public ReadOnlySpan Span { get; }
+
+ ///
+ public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; }
+
+ ///
+ public Vector2 ScreenOffset { get; }
+
+ ///
+ public ImFont* Font { get; }
+
+ ///
+ public float FontSize { get; }
+
+ /// Gets the multiplier value for glyph metrics, so that it scales to .
+ /// Multiplied to ,
+ /// , and distance values from
+ /// .
+ public float FontSizeScale { get; }
+
+ ///
+ public float LineHeight { get; }
+
+ ///
+ public float WrapWidth { get; }
+
+ ///
+ public float LinkUnderlineThickness { get; }
+
+ ///
+ public float Opacity { get; }
+
+ ///
+ public float EdgeOpacity { get; }
+
+ ///
+ public uint Color { get; set; }
+
+ ///
+ public uint EdgeColor { get; set; }
+
+ ///
+ public uint ShadowColor { get; set; }
+
+ ///
+ public uint LinkHoverBackColor { get; }
+
+ ///
+ public uint LinkActiveBackColor { get; }
+
+ ///
+ public bool ForceEdgeColor { get; }
+
+ ///
+ public bool Bold { get; set; }
+
+ ///
+ public bool Italic { get; set; }
+
+ ///
+ public bool Edge { get; set; }
+
+ ///
+ public bool Shadow { get; set; }
+
+ /// Gets a value indicating whether the edge should be drawn.
+ public readonly bool ShouldDrawEdge =>
+ (this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
+
+ /// Gets a value indicating whether the edge should be drawn.
+ public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
+
+ /// Gets a value indicating whether the edge should be drawn.
+ public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
+
+ /// Sets the current channel in the ImGui draw list splitter.
+ /// Channel to switch to.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
+ ImGuiNative.ImDrawListSplitter_SetCurrentChannel(this.splitter, this.drawList, (int)channelIndex);
+
+ /// Draws a single texture.
+ /// ImGui texture ID to draw from.
+ /// Offset of the glyph in pixels w.r.t. .
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.
+ /// Left top corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Color of the glyph in RGBA.
+ public readonly void Draw(
+ 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);
+ }
+
+ /// Draws a single texture.
+ /// ImGui texture ID to draw from.
+ /// Offset of the glyph in pixels w.r.t. .
+ /// Left top corner of the glyph w.r.t. its glyph origin in the target draw list.
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the target draw list.
+ /// Left top corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Right bottom corner of the glyph w.r.t. its glyph origin in the source texture.
+ /// Color of the glyph in RGBA.
+ /// Transformation for and that will push
+ /// top and bottom pixels to apply faux italicization by and
+ /// respectively.
+ public readonly void Draw(
+ 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);
+ }
+
+ /// Draws a single glyph using current styling configurations.
+ /// Glyph to draw.
+ /// Offset of the glyph in pixels w.r.t. .
+ internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
+ {
+ var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID;
+ var xy0 = new Vector2(
+ MathF.Round(g.X0 * this.FontSizeScale),
+ MathF.Round(g.Y0 * this.FontSizeScale));
+ var xy1 = new Vector2(
+ MathF.Round(g.X1 * this.FontSizeScale),
+ MathF.Round(g.Y1 * this.FontSizeScale));
+ var dxBold = this.Bold ? 2 : 1;
+ var dyItalic = this.Italic
+ ? new Vector2(this.FontSize - xy0.Y, this.FontSize - xy1.Y) / 6
+ : Vector2.Zero;
+ // Note: dyItalic values can be non-rounded; the glyph will be rendered sheared anyway.
+
+ offset.Y += MathF.Round((this.LineHeight - this.FontSize) / 2f);
+
+ if (this.ShouldDrawShadow)
+ {
+ this.SetCurrentChannel(SeStringDrawChannel.Shadow);
+ for (var i = 0; i < dxBold; i++)
+ this.Draw(texId, offset + new Vector2(i, 1), xy0, xy1, g.UV0, g.UV1, this.ShadowColor, dyItalic);
+ }
+
+ if (this.ShouldDrawEdge)
+ {
+ this.SetCurrentChannel(SeStringDrawChannel.Edge);
+
+ // Top & Bottom
+ for (var i = -1; i <= dxBold; i++)
+ {
+ this.Draw(texId, offset + new Vector2(i, -1), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic);
+ this.Draw(texId, offset + new Vector2(i, 1), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic);
+ }
+
+ // Left & Right
+ this.Draw(texId, offset + new Vector2(-1, 0), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic);
+ this.Draw(texId, offset + new Vector2(1, 0), xy0, xy1, g.UV0, g.UV1, this.EdgeColor, dyItalic);
+ }
+
+ if (this.ShouldDrawForeground)
+ {
+ this.SetCurrentChannel(SeStringDrawChannel.Foreground);
+ for (var i = 0; i < dxBold; i++)
+ this.Draw(texId, offset + new Vector2(i, 0), xy0, xy1, g.UV0, g.UV1, this.Color, dyItalic);
+ }
+ }
+
+ /// Draws an underline, for links.
+ /// Offset of the glyph in pixels w.r.t.
+ /// .
+ /// Advance width of the glyph.
+ internal 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);
+ }
+ }
+
+ /// Gets the glyph corresponding to the given codepoint.
+ /// An instance of that represents a character to display.
+ /// Corresponding glyph, or glyph of a fallback character specified from
+ /// .
+ internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
+ {
+ var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
+ ? ImGuiNative.ImFont_FindGlyph(this.Font, (ushort)rune.Value)
+ : this.Font->FallbackGlyph;
+ return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
+ }
+
+ /// Gets the glyph corresponding to the given codepoint.
+ /// An instance of that represents a character to display, that will be
+ /// changed on return to the rune corresponding to the fallback glyph if a glyph not corresponding to the
+ /// requested glyph is being returned.
+ /// Corresponding glyph, or glyph of a fallback character specified from
+ /// .
+ internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(ref Rune rune)
+ {
+ ref var glyph = ref this.FindGlyph(rune);
+ if (rune.Value != glyph.Codepoint && !Rune.TryCreate(glyph.Codepoint, out rune))
+ rune = Rune.ReplacementChar;
+ return ref glyph;
+ }
+
+ /// Gets the kerning adjustment between two glyphs in a succession corresponding to the given runes.
+ ///
+ /// Rune representing the glyph on the left side of a pair.
+ /// Rune representing the glyph on the right side of a pair.
+ /// Distance adjustment in pixels, scaled to the size specified from
+ /// , and rounded.
+ internal readonly float CalculateScaledDistance(Rune left, Rune right)
+ {
+ // Kerning distance entries are ignored if NUL, U+FFFF(invalid Unicode character), or characters outside
+ // the basic multilingual plane(BMP) is involved.
+ if (left.Value is <= 0 or >= char.MaxValue)
+ return 0;
+ if (right.Value is <= 0 or >= char.MaxValue)
+ return 0;
+
+ return MathF.Round(
+ ImGuiNative.ImFont_GetDistanceAdjustmentForPair(
+ this.Font,
+ (ushort)left.Value,
+ (ushort)right.Value) * this.FontSizeScale);
+ }
+
+ /// Handles style adjusting payloads.
+ /// Payload to handle.
+ /// true if the payload was handled.
+ internal bool HandleStyleAdjustingPayloads(ReadOnlySePayloadSpan payload)
+ {
+ switch (payload.MacroCode)
+ {
+ case MacroCode.Color:
+ this.colorStackSet.HandleColorPayload(ref this, payload);
+ return true;
+
+ case MacroCode.EdgeColor:
+ this.colorStackSet.HandleEdgeColorPayload(ref this, payload);
+ return true;
+
+ case MacroCode.ShadowColor:
+ this.colorStackSet.HandleShadowColorPayload(ref this, payload);
+ return true;
+
+ case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ // doesn't actually work in chat log
+ this.Bold = u != 0;
+ return true;
+
+ case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ this.Italic = u != 0;
+ return true;
+
+ case MacroCode.Edge when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ this.Edge = u != 0;
+ return true;
+
+ case MacroCode.Shadow when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
+ this.Shadow = u != 0;
+ return true;
+
+ case MacroCode.ColorType:
+ this.colorStackSet.HandleColorTypePayload(ref this, payload);
+ return true;
+
+ case MacroCode.EdgeColorType:
+ this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /// Splits the draw list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal readonly void SplitDrawList() =>
+ ImGuiNative.ImDrawListSplitter_Split(this.splitter, this.drawList, ChannelCount);
+
+ /// Merges the draw list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal readonly void MergeDrawList() => ImGuiNative.ImDrawListSplitter_Merge(this.splitter, this.drawList);
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringReplacementEntity.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringReplacementEntity.cs
new file mode 100644
index 000000000..b14e12073
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringReplacementEntity.cs
@@ -0,0 +1,48 @@
+using System.Numerics;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer;
+
+/// Replacement entity to draw instead while rendering a SeString.
+public readonly record struct SeStringReplacementEntity
+{
+ /// Initializes a new instance of the struct.
+ /// Number of bytes taken by this entity. Must be at least 0. If 0, then the entity
+ /// is considered as empty.
+ /// Size of this entity in pixels. Components must be non-negative.
+ /// Draw callback.
+ 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;
+ }
+
+ /// Gets the replacement entity.
+ /// Draw state.
+ /// Byte offset in .
+ /// Replacement entity definition, or default if none.
+ public delegate SeStringReplacementEntity GetEntityDelegate(scoped in SeStringDrawState state, int byteOffset);
+
+ /// Draws the replacement entity.
+ /// Draw state.
+ /// Byte offset in .
+ /// Relative offset in pixels w.r.t. .
+ public delegate void DrawDelegate(scoped in SeStringDrawState state, int byteOffset, Vector2 offset);
+
+ /// Gets the number of bytes taken by this entity.
+ public int ByteLength { get; init; }
+
+ /// Gets the size of this entity in pixels.
+ public Vector2 Size { get; init; }
+
+ /// Gets the Draw callback.
+ public DrawDelegate Draw { get; init; }
+
+ /// Gets a value indicating whether this entity is empty.
+ /// Instance of to test.
+ public static implicit operator bool(in SeStringReplacementEntity e) => e.ByteLength != 0;
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index 62c52a17f..d0dffce75 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using System.Numerics;
using System.Text;
using Dalamud.Data;
@@ -6,7 +7,9 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
+using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
@@ -32,6 +35,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
private ReadOnlySeString? logkind;
private SeStringDrawParams style;
private bool interactable;
+ private bool useEntity;
///
public string DisplayName { get; init; } = "SeStringRenderer Test";
@@ -45,12 +49,12 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
///
public void Load()
{
- this.style = default;
+ this.style = new() { GetEntity = this.GetEntity };
this.addons = null;
this.uicolor = null;
this.logkind = null;
this.testString = string.Empty;
- this.interactable = true;
+ this.interactable = this.useEntity = true;
this.Ready = true;
}
@@ -85,11 +89,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
var t3 = this.style.LineHeight ?? 1f;
if (ImGui.DragFloat("Line Height", ref t3, 0.01f, 0.4f, 3f, "%.02f"))
this.style.LineHeight = t3;
-
+
t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha;
if (ImGui.DragFloat("Opacity", ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.Opacity = t3;
-
+
t3 = this.style.EdgeStrength ?? 0.25f;
if (ImGui.DragFloat("Edge Strength", ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.EdgeStrength = t3;
@@ -123,11 +127,15 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Checkbox("Word Wrap", ref t))
this.style.WrapWidth = t ? null : float.PositiveInfinity;
- ImGui.SameLine();
t = this.interactable;
if (ImGui.Checkbox("Interactable", ref t))
this.interactable = t;
+ ImGui.SameLine();
+ t = this.useEntity;
+ if (ImGui.Checkbox("Use Entity Replacements", ref t))
+ this.useEntity = t;
+
if (ImGui.CollapsingHeader("UIColor Preview"))
{
if (this.uicolor is null)
@@ -267,7 +275,22 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped(
- "· For ease of testing, line breaks are automatically replaced to \\ .",
+ "Optional features implemented for the following test input: " +
+ "· line breaks are automatically replaced to \\ . " +
+ "· Dalamud will display Dalamud. " +
+ "· White will display White. " +
+ "· DefaultIcon will display DefaultIcon. " +
+ "· DisabledIcon will display DisabledIcon. " +
+ "· OutdatedInstallableIcon will display OutdatedInstallableIcon. " +
+ "· TroubleIcon will display TroubleIcon. " +
+ "· DevPluginIcon will display DevPluginIcon. " +
+ "· UpdateIcon will display UpdateIcon. " +
+ "· InstalledIcon will display InstalledIcon. " +
+ "· ThirdIcon will display ThirdIcon. " +
+ "· ThirdInstalledIcon will display ThirdInstalledIcon. " +
+ "· ChangelogApiBumpIcon will display ChangelogApiBumpIcon. " +
+ "· icon(5) will display icon(5). This is different from \\(5)>. " +
+ "· tex(ui/loadingimage/-nowloading_base25_hr1.tex) will display tex(ui/loadingimage/-nowloading_base25_hr1.tex).",
this.style);
ImGuiHelpers.ScaledDummy(3);
@@ -302,10 +325,14 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (this.interactable)
{
if (ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style, new("this is an ImGui id")) is
- { InteractedPayload: { } payload, InteractedPayloadOffset: var offset, InteractedPayloadEnvelope: var envelope } rr)
+ {
+ InteractedPayload: { } payload, InteractedPayloadOffset: var offset,
+ InteractedPayloadEnvelope: var envelope,
+ Clicked: var clicked
+ })
{
ImGui.TextUnformatted($"Hovered[{offset}]: {new ReadOnlySeStringSpan(envelope).ToString()}; {payload}");
- if (rr.Clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
+ if (clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
Util.OpenLink(dlp.ExtraString);
}
}
@@ -314,4 +341,138 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style);
}
}
+
+ private SeStringReplacementEntity GetEntity(scoped in SeStringDrawState state, int byteOffset)
+ {
+ if (!this.useEntity)
+ return default;
+ if (state.Span[byteOffset..].StartsWith("Dalamud"u8))
+ return new(7, new(state.FontSize, state.FontSize), DrawDalamud);
+ if (state.Span[byteOffset..].StartsWith("White"u8))
+ return new(5, new(state.FontSize, state.FontSize), DrawWhite);
+ if (state.Span[byteOffset..].StartsWith("DefaultIcon"u8))
+ return new(11, new(state.FontSize, state.FontSize), DrawDefaultIcon);
+ if (state.Span[byteOffset..].StartsWith("DisabledIcon"u8))
+ return new(12, new(state.FontSize, state.FontSize), DrawDisabledIcon);
+ if (state.Span[byteOffset..].StartsWith("OutdatedInstallableIcon"u8))
+ return new(23, new(state.FontSize, state.FontSize), DrawOutdatedInstallableIcon);
+ if (state.Span[byteOffset..].StartsWith("TroubleIcon"u8))
+ return new(11, new(state.FontSize, state.FontSize), DrawTroubleIcon);
+ if (state.Span[byteOffset..].StartsWith("DevPluginIcon"u8))
+ return new(13, new(state.FontSize, state.FontSize), DrawDevPluginIcon);
+ if (state.Span[byteOffset..].StartsWith("UpdateIcon"u8))
+ return new(10, new(state.FontSize, state.FontSize), DrawUpdateIcon);
+ if (state.Span[byteOffset..].StartsWith("ThirdIcon"u8))
+ return new(9, new(state.FontSize, state.FontSize), DrawThirdIcon);
+ if (state.Span[byteOffset..].StartsWith("ThirdInstalledIcon"u8))
+ return new(18, new(state.FontSize, state.FontSize), DrawThirdInstalledIcon);
+ if (state.Span[byteOffset..].StartsWith("ChangelogApiBumpIcon"u8))
+ return new(20, new(state.FontSize, state.FontSize), DrawChangelogApiBumpIcon);
+ if (state.Span[byteOffset..].StartsWith("InstalledIcon"u8))
+ return new(13, new(state.FontSize, state.FontSize), DrawInstalledIcon);
+ if (state.Span[byteOffset..].StartsWith("tex("u8))
+ {
+ var off = state.Span[byteOffset..].IndexOf((byte)')');
+ var tex = Service
+ .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
+ .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
+ .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
+ .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.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);
+ }
}