mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-13 12:14:16 +01:00
SeString renderer: Implement replacement entity (#1993)
* Refactor * Implement replacement entity * Apply rounding functions more correctly
This commit is contained in:
parent
23a2bd6228
commit
878b96e67d
10 changed files with 1199 additions and 672 deletions
|
|
@ -2,6 +2,7 @@ using System.Buffers.Binary;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Interface;
|
||||
|
||||
|
|
@ -247,6 +248,14 @@ public static class ColorHelpers
|
|||
public static uint Desaturate(uint color, float amount)
|
||||
=> RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount));
|
||||
|
||||
/// <summary>Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.</summary>
|
||||
/// <param name="rgba">RGBA value to transform.</param>
|
||||
/// <param name="opacity">Opacity to apply.</param>
|
||||
/// <returns>Transformed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint ApplyOpacity(uint rgba, float opacity) =>
|
||||
((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
|
||||
|
||||
/// <summary>
|
||||
/// Fade a color.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -145,5 +145,16 @@ internal sealed unsafe class GfdFile : FileResource
|
|||
|
||||
/// <summary>Gets the UV1 of the HQ version of this entry.</summary>
|
||||
public Vector2 HqUv1 => new((this.Left + this.Width) / 256f, (this.Top + this.Height + 170.5f) / 512f);
|
||||
|
||||
/// <summary>Calculates the size in pixels of a GFD entry when drawn along with a text.</summary>
|
||||
/// <param name="fontSize">Font size in pixels.</param>
|
||||
/// <param name="useHq">Whether to draw the HQ texture.</param>
|
||||
/// <returns>Determined size of the GFD entry when drawn.</returns>
|
||||
public readonly Vector2 CalculateScaledSize(float fontSize, out bool useHq)
|
||||
{
|
||||
useHq = fontSize > 19;
|
||||
var targetHeight = useHq ? fontSize : 20;
|
||||
return new(this.Width * (targetHeight / this.Height), targetHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
/// <summary>Parsed <c>gfdata.gfd</c> file, containing bitmap font icon lookup table.</summary>
|
||||
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>
|
||||
/// <remarks>Touched only from the main thread.</remarks>
|
||||
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>
|
||||
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 = [];
|
||||
private readonly SeStringColorStackSet colorStackSet;
|
||||
|
||||
/// <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();
|
||||
|
|
@ -113,29 +87,8 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
[ServiceManager.ServiceConstructor]
|
||||
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
|
||||
{
|
||||
var uiColor = dm.Excel.GetSheet<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);
|
||||
}
|
||||
|
||||
this.colorStackSet = new(
|
||||
dm.Excel.GetSheet<UIColor>() ?? throw new InvalidOperationException("Failed to access UIColor sheet."));
|
||||
this.gfd = dm.GetFile<GfdFile>("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,19 +237,22 @@ 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];
|
||||
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<byte>.Empty : sss.Data[hoveredLinkOffset..]).GetEnumerator();
|
||||
|
|
@ -412,18 +358,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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()
|
||||
{
|
||||
if (this.splitter is not null)
|
||||
|
|
@ -437,25 +371,74 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
/// <param name="state">Draw state.</param>
|
||||
/// <param name="baseY">Y offset adjustment for all text fragments. Used to honor
|
||||
/// <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 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 =
|
||||
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) &&
|
||||
e.TryGetUInt(out var u) &&
|
||||
u == (uint)LinkMacroPayloadType.Terminator
|
||||
|
|
@ -466,37 +449,55 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
if (p.Offset != 0)
|
||||
{
|
||||
curr = prev + p.Offset;
|
||||
break2 = true;
|
||||
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.
|
||||
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)
|
||||
{
|
||||
// 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.Params.LineHeight;
|
||||
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;
|
||||
overflows = fragment.VisibleWidth > state.WrapWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (overflows)
|
||||
{
|
||||
// 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.Params.WrapWidth - xy.X;
|
||||
fragment = state.CreateFragment(this, prev, curr, true, xy, linkOffset, remainingWidth);
|
||||
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
|
|||
/// <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="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(
|
||||
ref DrawState state,
|
||||
ref SeStringDrawState state,
|
||||
Vector2 offset,
|
||||
bool displaySoftHyphen,
|
||||
ReadOnlySpan<byte> span,
|
||||
|
|
@ -559,186 +560,47 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
if (!enu.MoveNext())
|
||||
continue;
|
||||
|
||||
var payload = 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)
|
||||
if (state.HandleStyleAdjustingPayloads(enu.Current.Payload))
|
||||
continue;
|
||||
|
||||
var size = state.CalculateGfdEntrySize(gfdEntry, out var useHq);
|
||||
state.SetCurrentChannel(ChannelFore);
|
||||
if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is var icon and not None &&
|
||||
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(
|
||||
offset + new Vector2(x, MathF.Round((state.Params.LineHeight - size.Y) / 2)),
|
||||
gfdTextureSrv,
|
||||
Vector2.Zero,
|
||||
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
|
||||
size,
|
||||
Vector2.Zero,
|
||||
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
|
||||
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
|
||||
ApplyOpacityValue(uint.MaxValue, state.Params.Opacity));
|
||||
ColorHelpers.ApplyOpacity(uint.MaxValue, state.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;
|
||||
}
|
||||
}
|
||||
|
||||
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<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];
|
||||
x += dist + advanceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -831,194 +693,48 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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>
|
||||
/// <param name="renderer">Associated renderer.</param>
|
||||
/// <param name="from">Starting byte offset (inclusive) in <see cref="Raw"/> that this fragment deals with.
|
||||
/// </param>
|
||||
/// <param name="to">Ending byte offset (exclusive) in <see cref="Raw"/> that this fragment deals with.</param>
|
||||
/// <param name="state">Draw state.</param>
|
||||
/// <param name="from">Starting byte offset (inclusive) in <see cref="SeStringDrawState.Span"/> 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="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
|
||||
/// text fragment.</param>
|
||||
/// <param name="link">Byte offset of the link payload in <see cref="SeStringDrawState.Span"/> that
|
||||
/// 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
|
||||
/// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
|
||||
/// the wrap width.</param>
|
||||
/// <returns>Newly created text fragment.</returns>
|
||||
public readonly TextFragment CreateFragment(
|
||||
SeStringRenderer renderer,
|
||||
private TextFragment CreateFragment(
|
||||
scoped in SeStringDrawState state,
|
||||
int from,
|
||||
int to,
|
||||
bool breakAfter,
|
||||
Vector2 offset,
|
||||
int activeLinkOffset,
|
||||
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;
|
||||
|
|
@ -1028,19 +744,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
var lastDisplayRune = default(Rune);
|
||||
var lastNonSoftHyphenRune = default(Rune);
|
||||
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 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) &&
|
||||
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 = this.CalculateGfdEntrySize(gfdEntry, out _);
|
||||
var size = gfdEntry.CalculateScaledSize(state.FontSize, out _);
|
||||
w = Math.Max(w, x + size.X);
|
||||
x += MathF.Round(size.X);
|
||||
displayRune = default;
|
||||
|
|
@ -1048,10 +764,10 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
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);
|
||||
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) &&
|
||||
|
|
@ -1091,8 +807,9 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
return new(
|
||||
from,
|
||||
to,
|
||||
activeLinkOffset,
|
||||
link,
|
||||
offset,
|
||||
entity,
|
||||
visibleWidth,
|
||||
advanceWidth,
|
||||
advanceWidthWithoutSoftHyphen,
|
||||
|
|
@ -1102,52 +819,37 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
|
|||
lastNonSoftHyphenRune);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
|
||||
/// <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="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
|
||||
? 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);
|
||||
}
|
||||
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
|||
internal ref struct LineBreakEnumerator
|
||||
{
|
||||
private readonly UtfEnumeratorFlags enumeratorFlags;
|
||||
private readonly int dataLength;
|
||||
|
||||
private UtfEnumerator enumerator;
|
||||
private int dataLength;
|
||||
private int currentByteOffsetDelta;
|
||||
|
||||
private Entry class1;
|
||||
private Entry class2;
|
||||
|
||||
|
|
@ -24,8 +26,6 @@ internal ref struct LineBreakEnumerator
|
|||
|
||||
private int consecutiveRegionalIndicators;
|
||||
|
||||
private bool finished;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary>
|
||||
/// <param name="data">UTF-N byte sequence.</param>
|
||||
/// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param>
|
||||
|
|
@ -58,11 +58,25 @@ internal ref struct LineBreakEnumerator
|
|||
/// <inheritdoc cref="IEnumerator{T}.Current"/>
|
||||
public (int ByteOffset, bool Mandatory) Current { get; private set; }
|
||||
|
||||
/// <summary>Gets a value indicating whether the end of the underlying span has been reached.</summary>
|
||||
public bool Finished { get; private set; }
|
||||
|
||||
/// <summary>Resumes enumeration with the given data.</summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="offsetDelta">Offset to add to <see cref="Current"/>.<c>ByteOffset</c>.</param>
|
||||
public void ResumeWith(ReadOnlySpan<byte> data, int offsetDelta)
|
||||
{
|
||||
this.enumerator = UtfEnumerator.From(data, this.enumeratorFlags);
|
||||
this.dataLength = data.Length;
|
||||
this.currentByteOffsetDelta = offsetDelta;
|
||||
this.Finished = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerator.MoveNext"/>
|
||||
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.finished)
|
||||
if (this.Finished)
|
||||
return false;
|
||||
|
||||
while (this.enumerator.MoveNext())
|
||||
|
|
@ -77,10 +91,10 @@ internal ref struct LineBreakEnumerator
|
|||
switch (this.HandleCharacter(effectiveInt))
|
||||
{
|
||||
case LineBreakMode.Mandatory:
|
||||
this.Current = (this.enumerator.Current.ByteOffset, true);
|
||||
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, true);
|
||||
return true;
|
||||
case LineBreakMode.Optional:
|
||||
this.Current = (this.enumerator.Current.ByteOffset, false);
|
||||
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, false);
|
||||
return true;
|
||||
case LineBreakMode.Prohibited:
|
||||
default:
|
||||
|
|
@ -90,8 +104,8 @@ internal ref struct LineBreakEnumerator
|
|||
|
||||
// Start and end of text:
|
||||
// LB3 Always break at the end of text.
|
||||
this.Current = (this.dataLength, true);
|
||||
this.finished = true;
|
||||
this.Current = (this.dataLength + this.currentByteOffsetDelta, true);
|
||||
this.Finished = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -18,14 +16,18 @@ public record struct SeStringDrawParams
|
|||
/// </remarks>
|
||||
public ImDrawListPtr? TargetDrawList { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the font to use.</summary>
|
||||
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value>
|
||||
public ImFontPtr? Font { get; set; }
|
||||
/// <summary>Gets or sets the function to be called on every codepoint and payload for the purpose of offering
|
||||
/// chances to draw something else instead of glyphs or SeString payload entities.</summary>
|
||||
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the screen offset of the left top corner.</summary>
|
||||
/// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos"/>.</value>
|
||||
public Vector2? ScreenOffset { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the font to use.</summary>
|
||||
/// <value>Font to use, or <c>null</c> to use <see cref="ImGui.GetFont"/> (the default).</value>
|
||||
public ImFontPtr? Font { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the font size.</summary>
|
||||
/// <value>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>.
|
||||
/// </value>
|
||||
|
|
@ -86,83 +88,23 @@ public record struct SeStringDrawParams
|
|||
public bool Italic { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the text is rendered with edge.</summary>
|
||||
/// <remarks>If an edge color is pushed with <see cref="MacroCode.EdgeColor"/> or
|
||||
/// <see cref="MacroCode.EdgeColorType"/>, it will be drawn regardless. Set <see cref="ForceEdgeColor"/> to
|
||||
/// <c>true</c> and set <see cref="EdgeColor"/> to <c>0</c> to fully disable edge.</remarks>
|
||||
public bool Edge { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the text is rendered with shadow.</summary>
|
||||
public bool Shadow { get; set; }
|
||||
|
||||
private readonly unsafe ImFont* EffectiveFont =>
|
||||
/// <summary>Gets the effective font.</summary>
|
||||
internal readonly unsafe ImFont* EffectiveFont =>
|
||||
(this.Font ?? ImGui.GetFont()) is var f && f.NativePtr is not null
|
||||
? f.NativePtr
|
||||
: throw new ArgumentException("Specified font is empty.");
|
||||
|
||||
private readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
|
||||
/// <summary>Gets the effective line height in pixels.</summary>
|
||||
internal readonly float EffectiveLineHeight => (this.FontSize ?? ImGui.GetFontSize()) * (this.LineHeight ?? 1f);
|
||||
|
||||
private readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
|
||||
|
||||
/// <summary>Calculated values from <see cref="SeStringDrawParams"/> using ImGui styles.</summary>
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.OrderingRules",
|
||||
"SA1214:Readonly fields should appear before non-readonly fields",
|
||||
Justification = "Matching the above order.")]
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe struct Resolved(in SeStringDrawParams ssdp)
|
||||
{
|
||||
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
|
||||
public readonly ImDrawList* DrawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Font"/>
|
||||
public readonly ImFont* Font = ssdp.EffectiveFont;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.ScreenOffset"/>
|
||||
public readonly Vector2 ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.FontSize"/>
|
||||
public readonly float FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.LineHeight"/>
|
||||
public readonly float LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.WrapWidth"/>
|
||||
public readonly float WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.LinkUnderlineThickness"/>
|
||||
public readonly float LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Opacity"/>
|
||||
public readonly float Opacity = ssdp.EffectiveOpacity;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.EdgeStrength"/>
|
||||
public readonly float EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Color"/>
|
||||
public uint Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.EdgeColor"/>
|
||||
public uint EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.ShadowColor"/>
|
||||
public uint ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.LinkHoverBackColor"/>
|
||||
public readonly uint LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.LinkActiveBackColor"/>
|
||||
public readonly uint LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.ForceEdgeColor"/>
|
||||
public readonly bool ForceEdgeColor = ssdp.ForceEdgeColor;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Bold"/>
|
||||
public bool Bold = ssdp.Bold;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Italic"/>
|
||||
public bool Italic = ssdp.Italic;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Edge"/>
|
||||
public bool Edge = ssdp.Edge;
|
||||
|
||||
/// <inheritdoc cref="SeStringDrawParams.Shadow"/>
|
||||
public bool Shadow = ssdp.Shadow;
|
||||
}
|
||||
/// <summary>Gets the effective opacity.</summary>
|
||||
internal readonly float EffectiveOpacity => this.Opacity ?? ImGui.GetStyle().Alpha;
|
||||
}
|
||||
|
|
|
|||
400
Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
Normal file
400
Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Data;
|
||||
|
|
@ -6,7 +7,9 @@ using Dalamud.Game.Gui;
|
|||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Textures.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Storage.Assets;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
|
@ -32,6 +35,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
private ReadOnlySeString? logkind;
|
||||
private SeStringDrawParams style;
|
||||
private bool interactable;
|
||||
private bool useEntity;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "SeStringRenderer Test";
|
||||
|
|
@ -45,12 +49,12 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
{
|
||||
this.style = default;
|
||||
this.style = new() { GetEntity = this.GetEntity };
|
||||
this.addons = null;
|
||||
this.uicolor = null;
|
||||
this.logkind = null;
|
||||
this.testString = string.Empty;
|
||||
this.interactable = true;
|
||||
this.interactable = this.useEntity = true;
|
||||
this.Ready = true;
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +127,15 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
if (ImGui.Checkbox("Word Wrap", ref t))
|
||||
this.style.WrapWidth = t ? null : float.PositiveInfinity;
|
||||
|
||||
ImGui.SameLine();
|
||||
t = this.interactable;
|
||||
if (ImGui.Checkbox("Interactable", ref t))
|
||||
this.interactable = t;
|
||||
|
||||
ImGui.SameLine();
|
||||
t = this.useEntity;
|
||||
if (ImGui.Checkbox("Use Entity Replacements", ref t))
|
||||
this.useEntity = t;
|
||||
|
||||
if (ImGui.CollapsingHeader("UIColor Preview"))
|
||||
{
|
||||
if (this.uicolor is null)
|
||||
|
|
@ -267,7 +275,22 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
|
||||
ImGuiHelpers.ScaledDummy(3);
|
||||
ImGuiHelpers.CompileSeStringWrapped(
|
||||
"· For ease of testing, <colortype(506)><edgecolortype(507)>line breaks<colortype(0)><edgecolortype(0)> are automatically replaced to <colortype(502)><edgecolortype(503)>\\<br><colortype(0)><edgecolortype(0)>.",
|
||||
"Optional features implemented for the following test input:<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>line breaks<colortype(0)><edgecolortype(0)> are automatically replaced to <colortype(502)><edgecolortype(503)>\\<br><colortype(0)><edgecolortype(0)>.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>alamud<colortype(0)><edgecolortype(0)> will display Dalamud.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>W<link(0xCE)>hite<colortype(0)><edgecolortype(0)> will display White.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>efaultIcon<colortype(0)><edgecolortype(0)> will display DefaultIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>isabledIcon<colortype(0)><edgecolortype(0)> will display DisabledIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>O<link(0xCE)>utdatedInstallableIcon<colortype(0)><edgecolortype(0)> will display OutdatedInstallableIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>roubleIcon<colortype(0)><edgecolortype(0)> will display TroubleIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>D<link(0xCE)>evPluginIcon<colortype(0)><edgecolortype(0)> will display DevPluginIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>U<link(0xCE)>pdateIcon<colortype(0)><edgecolortype(0)> will display UpdateIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>I<link(0xCE)>nstalledIcon<colortype(0)><edgecolortype(0)> will display InstalledIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>hirdIcon<colortype(0)><edgecolortype(0)> will display ThirdIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>T<link(0xCE)>hirdInst<link(0xCE)>alledIcon<colortype(0)><edgecolortype(0)> will display ThirdInstalledIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>C<link(0xCE)>hangelogApiBumpIcon<colortype(0)><edgecolortype(0)> will display ChangelogApiBumpIcon.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>icon<link(0xCE)>(5)<colortype(0)><edgecolortype(0)> will display icon(5). This is different from \\<icon<link(0xCE)>(5)>.<br>" +
|
||||
"· <colortype(506)><edgecolortype(507)>tex<link(0xCE)>(ui/loadingimage/-nowloading_base25_hr1.tex)<colortype(0)><edgecolortype(0)> will display tex(ui/loadingimage/-nowloading_base25_hr1.tex).",
|
||||
this.style);
|
||||
ImGuiHelpers.ScaledDummy(3);
|
||||
|
||||
|
|
@ -302,10 +325,14 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
if (this.interactable)
|
||||
{
|
||||
if (ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style, new("this is an ImGui id")) is
|
||||
{ InteractedPayload: { } payload, InteractedPayloadOffset: var offset, InteractedPayloadEnvelope: var envelope } rr)
|
||||
{
|
||||
InteractedPayload: { } payload, InteractedPayloadOffset: var offset,
|
||||
InteractedPayloadEnvelope: var envelope,
|
||||
Clicked: var clicked
|
||||
})
|
||||
{
|
||||
ImGui.TextUnformatted($"Hovered[{offset}]: {new ReadOnlySeStringSpan(envelope).ToString()}; {payload}");
|
||||
if (rr.Clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
|
||||
if (clicked && payload is DalamudLinkPayload { Plugin: "test" } dlp)
|
||||
Util.OpenLink(dlp.ExtraString);
|
||||
}
|
||||
}
|
||||
|
|
@ -314,4 +341,138 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style);
|
||||
}
|
||||
}
|
||||
|
||||
private SeStringReplacementEntity GetEntity(scoped in SeStringDrawState state, int byteOffset)
|
||||
{
|
||||
if (!this.useEntity)
|
||||
return default;
|
||||
if (state.Span[byteOffset..].StartsWith("Dalamud"u8))
|
||||
return new(7, new(state.FontSize, state.FontSize), DrawDalamud);
|
||||
if (state.Span[byteOffset..].StartsWith("White"u8))
|
||||
return new(5, new(state.FontSize, state.FontSize), DrawWhite);
|
||||
if (state.Span[byteOffset..].StartsWith("DefaultIcon"u8))
|
||||
return new(11, new(state.FontSize, state.FontSize), DrawDefaultIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("DisabledIcon"u8))
|
||||
return new(12, new(state.FontSize, state.FontSize), DrawDisabledIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("OutdatedInstallableIcon"u8))
|
||||
return new(23, new(state.FontSize, state.FontSize), DrawOutdatedInstallableIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("TroubleIcon"u8))
|
||||
return new(11, new(state.FontSize, state.FontSize), DrawTroubleIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("DevPluginIcon"u8))
|
||||
return new(13, new(state.FontSize, state.FontSize), DrawDevPluginIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("UpdateIcon"u8))
|
||||
return new(10, new(state.FontSize, state.FontSize), DrawUpdateIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("ThirdIcon"u8))
|
||||
return new(9, new(state.FontSize, state.FontSize), DrawThirdIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("ThirdInstalledIcon"u8))
|
||||
return new(18, new(state.FontSize, state.FontSize), DrawThirdInstalledIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("ChangelogApiBumpIcon"u8))
|
||||
return new(20, new(state.FontSize, state.FontSize), DrawChangelogApiBumpIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("InstalledIcon"u8))
|
||||
return new(13, new(state.FontSize, state.FontSize), DrawInstalledIcon);
|
||||
if (state.Span[byteOffset..].StartsWith("tex("u8))
|
||||
{
|
||||
var off = state.Span[byteOffset..].IndexOf((byte)')');
|
||||
var tex = Service<TextureManager>
|
||||
.Get()
|
||||
.Shared
|
||||
.GetFromGame(Encoding.UTF8.GetString(state.Span[(byteOffset + 4)..(byteOffset + off)]))
|
||||
.GetWrapOrEmpty();
|
||||
return new(off + 1, tex.Size * (state.FontSize / tex.Size.Y), DrawTexture);
|
||||
}
|
||||
|
||||
if (state.Span[byteOffset..].StartsWith("icon("u8))
|
||||
{
|
||||
var off = state.Span[byteOffset..].IndexOf((byte)')');
|
||||
if (int.TryParse(state.Span[(byteOffset + 5)..(byteOffset + off)], out var parsed))
|
||||
{
|
||||
var tex = Service<TextureManager>
|
||||
.Get()
|
||||
.Shared
|
||||
.GetFromGameIcon(parsed)
|
||||
.GetWrapOrEmpty();
|
||||
return new(off + 1, tex.Size * (state.FontSize / tex.Size.Y), DrawIcon);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static void DrawTexture(scoped in SeStringDrawState state, int byteOffset, Vector2 offset)
|
||||
{
|
||||
var off = state.Span[byteOffset..].IndexOf((byte)')');
|
||||
var tex = Service<TextureManager>
|
||||
.Get()
|
||||
.Shared
|
||||
.GetFromGame(Encoding.UTF8.GetString(state.Span[(byteOffset + 4)..(byteOffset + off)]))
|
||||
.GetWrapOrEmpty();
|
||||
state.Draw(
|
||||
tex.ImGuiHandle,
|
||||
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
|
||||
tex.Size * (state.FontSize / tex.Size.Y),
|
||||
Vector2.Zero,
|
||||
Vector2.One);
|
||||
}
|
||||
|
||||
static void DrawIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset)
|
||||
{
|
||||
var off = state.Span[byteOffset..].IndexOf((byte)')');
|
||||
if (!int.TryParse(state.Span[(byteOffset + 5)..(byteOffset + off)], out var parsed))
|
||||
return;
|
||||
var tex = Service<TextureManager>
|
||||
.Get()
|
||||
.Shared
|
||||
.GetFromGameIcon(parsed)
|
||||
.GetWrapOrEmpty();
|
||||
state.Draw(
|
||||
tex.ImGuiHandle,
|
||||
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
|
||||
tex.Size * (state.FontSize / tex.Size.Y),
|
||||
Vector2.Zero,
|
||||
Vector2.One);
|
||||
}
|
||||
|
||||
static void DrawAsset(scoped in SeStringDrawState state, Vector2 offset, DalamudAsset asset) =>
|
||||
state.Draw(
|
||||
Service<DalamudAssetManager>.Get().GetDalamudTextureWrap(asset).ImGuiHandle,
|
||||
offset + new Vector2(0, (state.LineHeight - state.FontSize) / 2),
|
||||
new(state.FontSize, state.FontSize),
|
||||
Vector2.Zero,
|
||||
Vector2.One);
|
||||
|
||||
static void DrawDalamud(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.LogoSmall);
|
||||
|
||||
static void DrawWhite(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.White4X4);
|
||||
|
||||
static void DrawDefaultIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.DefaultIcon);
|
||||
|
||||
static void DrawDisabledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.DisabledIcon);
|
||||
|
||||
static void DrawOutdatedInstallableIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.OutdatedInstallableIcon);
|
||||
|
||||
static void DrawTroubleIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.TroubleIcon);
|
||||
|
||||
static void DrawDevPluginIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.DevPluginIcon);
|
||||
|
||||
static void DrawUpdateIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.UpdateIcon);
|
||||
|
||||
static void DrawInstalledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.InstalledIcon);
|
||||
|
||||
static void DrawThirdIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.ThirdIcon);
|
||||
|
||||
static void DrawThirdInstalledIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.ThirdInstalledIcon);
|
||||
|
||||
static void DrawChangelogApiBumpIcon(scoped in SeStringDrawState state, int byteOffset, Vector2 offset) =>
|
||||
DrawAsset(state, offset, DalamudAsset.ChangelogApiBumpIcon);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue