mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
SeString renderer: fix colors, add link support (#1983)
* Add coloring options * Add link support * simplify * fixes * Prevent EncodeString from causing crashes * Fix link range application and add link example * Fix test widget * Make DalamudLinkPayload backward compatible * make it better to use * make it better to use * Mark SeString rendering functions experimental via comments * rename * Simplify * Make sestring draw functions take in draw params * Improvements
This commit is contained in:
parent
5fdd88b488
commit
b6eb18d550
25 changed files with 2009 additions and 766 deletions
|
|
@ -115,10 +115,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\EastAsianWidth.txt" LogicalName="EastAsianWidth.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\DerivedGeneralCategory.txt" LogicalName="DerivedGeneralCategory.txt" />
|
||||
<EmbeddedResource Include="Interface\Internal\ImGuiSeStringRenderer\TextProcessing\emoji-data.txt" LogicalName="emoji-data.txt" />
|
||||
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\DerivedGeneralCategory.txt" LogicalName="DerivedGeneralCategory.txt" />
|
||||
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\EastAsianWidth.txt" LogicalName="EastAsianWidth.txt" />
|
||||
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\emoji-data.txt" LogicalName="emoji-data.txt" />
|
||||
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.IO;
|
|||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -13,32 +15,39 @@ public class DalamudLinkPayload : Payload
|
|||
/// <inheritdoc/>
|
||||
public override PayloadType Type => PayloadType.DalamudLink;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin command ID to be linked.
|
||||
/// </summary>
|
||||
public uint CommandId { get; internal set; } = 0;
|
||||
/// <summary>Gets the plugin command ID to be linked.</summary>
|
||||
public uint CommandId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name to be linked.
|
||||
/// </summary>
|
||||
/// <summary>Gets an optional extra integer value 1.</summary>
|
||||
public int Extra1 { get; internal set; }
|
||||
|
||||
/// <summary>Gets an optional extra integer value 2.</summary>
|
||||
public int Extra2 { get; internal set; }
|
||||
|
||||
/// <summary>Gets the plugin name to be linked.</summary>
|
||||
public string Plugin { get; internal set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets an optional extra string.</summary>
|
||||
public string ExtraString { get; internal set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
|
||||
}
|
||||
public override string ToString() =>
|
||||
$"{this.Type} - {this.Plugin} ({this.CommandId}/{this.Extra1}/{this.Extra2}/{this.ExtraString})";
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
return new Lumina.Text.SeStringBuilder()
|
||||
.BeginMacro(MacroCode.Link)
|
||||
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
|
||||
.AppendStringExpression(this.Plugin)
|
||||
.AppendUIntExpression(this.CommandId)
|
||||
.EndMacro()
|
||||
.ToArray();
|
||||
.BeginMacro(MacroCode.Link)
|
||||
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
|
||||
.AppendUIntExpression(this.CommandId)
|
||||
.AppendIntExpression(this.Extra1)
|
||||
.AppendIntExpression(this.Extra2)
|
||||
.BeginStringExpression()
|
||||
.Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString }))
|
||||
.EndExpression()
|
||||
.EndMacro()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -49,16 +58,53 @@ public class DalamudLinkPayload : Payload
|
|||
var body = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
|
||||
var rosps = new ReadOnlySePayloadSpan(ReadOnlySePayloadType.Macro, MacroCode.Link, body.AsSpan());
|
||||
|
||||
if (!rosps.TryGetExpression(out var pluginExpression, out var commandIdExpression))
|
||||
return;
|
||||
if (!rosps.TryGetExpression(
|
||||
out var commandIdExpression,
|
||||
out var extra1Expression,
|
||||
out var extra2Expression,
|
||||
out var compositeExpression))
|
||||
{
|
||||
if (!rosps.TryGetExpression(out var pluginExpression, out commandIdExpression))
|
||||
return;
|
||||
|
||||
if (!pluginExpression.TryGetString(out var pluginString))
|
||||
return;
|
||||
if (!pluginExpression.TryGetString(out var pluginString))
|
||||
return;
|
||||
|
||||
if (!commandIdExpression.TryGetUInt(out var commandId))
|
||||
return;
|
||||
if (!commandIdExpression.TryGetUInt(out var commandId))
|
||||
return;
|
||||
|
||||
this.Plugin = pluginString.ExtractText();
|
||||
this.CommandId = commandId;
|
||||
this.Plugin = pluginString.ExtractText();
|
||||
this.CommandId = commandId;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!commandIdExpression.TryGetUInt(out var commandId))
|
||||
return;
|
||||
|
||||
if (!extra1Expression.TryGetInt(out var extra1))
|
||||
return;
|
||||
|
||||
if (!extra2Expression.TryGetInt(out var extra2))
|
||||
return;
|
||||
|
||||
if (!compositeExpression.TryGetString(out var compositeString))
|
||||
return;
|
||||
|
||||
string[] extraData;
|
||||
try
|
||||
{
|
||||
extraData = JsonConvert.DeserializeObject<string[]>(compositeString.ExtractText());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.CommandId = commandId;
|
||||
this.Extra1 = extra1;
|
||||
this.Extra2 = extra2;
|
||||
this.Plugin = extraData[0];
|
||||
this.ExtraString = extraData[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
|
|||
|
||||
using Lumina.Data;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
|
||||
/// <summary>Game font data file.</summary>
|
||||
internal sealed unsafe class GfdFile : FileResource
|
||||
1149
Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
Normal file
1149
Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,11 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeEastAsianWidthClass;
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeGeneralCategory;
|
||||
using static Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing.UnicodeLineBreakClass;
|
||||
using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeEastAsianWidthClass;
|
||||
using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeGeneralCategory;
|
||||
using static Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing.UnicodeLineBreakClass;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary>Enumerates line break offsets.</summary>
|
||||
internal ref struct LineBreakEnumerator
|
||||
|
|
@ -3,7 +3,7 @@ using System.IO;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary>Stores unicode data.</summary>
|
||||
internal static class UnicodeData
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary><a href="https://www.unicode.org/reports/tr11/">Unicode east asian width</a>.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary><a href="https://www.unicode.org/reports/tr51/#Emoji_Characters">Unicode emoji property</a>.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary><a href="https://www.unicode.org/reports/tr44/#General_Category_Values">Unicode general category.</a>.
|
||||
/// </summary>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary><a href="https://unicode.org/reports/tr14/#Table1">Unicode line break class</a>.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Unicode Data")]
|
||||
|
|
@ -8,7 +8,7 @@ using Lumina.Text;
|
|||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary>Enumerates a UTF-N byte sequence by codepoint.</summary>
|
||||
[DebuggerDisplay("{Current}/{data.Length} ({flags}, BE={isBigEndian})")]
|
||||
|
|
@ -257,7 +257,11 @@ internal ref struct UtfEnumerator
|
|||
/// <summary>Gets the effective <c>char</c> value, with invalid or non-representable codepoints replaced.
|
||||
/// </summary>
|
||||
/// <value><see cref="char.MaxValue"/> if the character should not be displayed at all.</value>
|
||||
public char EffectiveChar => this.EffectiveInt is var i and >= 0 and < char.MaxValue ? (char)i : char.MaxValue;
|
||||
public char EffectiveChar
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.EffectiveInt is var i and >= 0 and < char.MaxValue ? (char)i : char.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the effective <c>int</c> value, with invalid codepoints replaced.</summary>
|
||||
/// <value><see cref="char.MaxValue"/> if the character should not be displayed at all.</value>
|
||||
|
|
@ -268,6 +272,14 @@ internal ref struct UtfEnumerator
|
|||
? 0xFFFD
|
||||
: rune.Value;
|
||||
|
||||
/// <summary>Gets the effective <see cref="Rune"/> value, with invalid codepoints replaced.</summary>
|
||||
/// <value><see cref="char.MaxValue"/> if the character should not be displayed at all.</value>
|
||||
public Rune EffectiveRune
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(this.EffectiveInt);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(Subsequence left, Subsequence right) => left.Equals(right);
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary>Flags on enumerating a unicode sequence.</summary>
|
||||
[Flags]
|
||||
|
|
@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
|
||||
|
||||
/// <summary>Represents a single value to be used in a UTF-N byte sequence.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 4)]
|
||||
168
Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
Normal file
168
Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Text.Payloads;
|
||||
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
|
||||
/// <summary>Render styles for a SeString.</summary>
|
||||
public record struct SeStringDrawParams
|
||||
{
|
||||
/// <summary>Gets or sets the target draw list.</summary>
|
||||
/// <value>Target draw list, <c>default(ImDrawListPtr)</c> to not draw, or <c>null</c> to use
|
||||
/// <see cref="ImGui.GetWindowDrawList"/> (the default).</value>
|
||||
/// <remarks>If this value is set, <see cref="ImGui.Dummy"/> will not be called, and ImGui ID will be ignored.
|
||||
/// </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 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 size.</summary>
|
||||
/// <value>Font size in pixels, or <c>0</c> to use the current ImGui font size <see cref="ImGui.GetFontSize"/>.
|
||||
/// </value>
|
||||
public float? FontSize { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the line height ratio.</summary>
|
||||
/// <value><c>1</c> or <c>null</c> (the default) will use <see cref="FontSize"/> as the line height.
|
||||
/// <c>2</c> will make line height twice the <see cref="FontSize"/>.</value>
|
||||
public float? LineHeight { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the wrapping width.</summary>
|
||||
/// <value>Width in pixels, or <c>null</c> to wrap at the end of available content region from
|
||||
/// <see cref="ImGui.GetContentRegionAvail"/> (the default).</value>
|
||||
public float? WrapWidth { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the thickness of underline under links.</summary>
|
||||
public float? LinkUnderlineThickness { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the opacity, commonly called "alpha".</summary>
|
||||
/// <value>Opacity value ranging from 0(invisible) to 1(fully visible), or <c>null</c> to use the current ImGui
|
||||
/// opacity from <see cref="ImGuiStyle.Alpha"/> accessed using <see cref="ImGui.GetStyle"/>.</value>
|
||||
public float? Opacity { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the strength of the edge, which will have effects on the edge opacity.</summary>
|
||||
/// <value>Strength value ranging from 0(invisible) to 1(fully visible), or <c>null</c> to use the default value
|
||||
/// of <c>0.25f</c> that might be subject to change in the future.</value>
|
||||
public float? EdgeStrength { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the color of the rendered text.</summary>
|
||||
/// <value>Color in RGBA, or <c>null</c> to use <see cref="ImGuiCol.Text"/> (the default).</value>
|
||||
public uint? Color { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the color of the rendered text edge.</summary>
|
||||
/// <value>Color in RGBA, or <c>null</c> to use opaque black (the default).</value>
|
||||
public uint? EdgeColor { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the color of the rendered text shadow.</summary>
|
||||
/// <value>Color in RGBA, or <c>null</c> to use opaque black (the default).</value>
|
||||
public uint? ShadowColor { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the background color of a link when hovered.</summary>
|
||||
/// <value>Color in RGBA, or <c>null</c> to use <see cref="ImGuiCol.ButtonHovered"/> (the default).</value>
|
||||
public uint? LinkHoverBackColor { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the background color of a link when active.</summary>
|
||||
/// <value>Color in RGBA, or <c>null</c> to use <see cref="ImGuiCol.ButtonActive"/> (the default).</value>
|
||||
public uint? LinkActiveBackColor { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether to force the color of the rendered text edge.</summary>
|
||||
/// <remarks>If set, then <see cref="MacroCode.EdgeColor"/> and <see cref="MacroCode.EdgeColorType"/> will be
|
||||
/// ignored.</remarks>
|
||||
public bool ForceEdgeColor { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the text is rendered bold.</summary>
|
||||
public bool Bold { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the text is rendered italic.</summary>
|
||||
public bool Italic { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the text is rendered with edge.</summary>
|
||||
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 =>
|
||||
(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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
namespace Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
|
||||
/// <summary>Represents the result of n rendered interactable SeString.</summary>
|
||||
public ref struct SeStringDrawResult
|
||||
{
|
||||
private Payload? lazyPayload;
|
||||
|
||||
/// <summary>Gets the visible size of the text rendered/to be rendered.</summary>
|
||||
public Vector2 Size { get; init; }
|
||||
|
||||
/// <summary>Gets a value indicating whether a payload or the whole text has been clicked.</summary>
|
||||
public bool Clicked { get; init; }
|
||||
|
||||
/// <summary>Gets the offset of the interacted payload, or <c>-1</c> if none.</summary>
|
||||
public int InteractedPayloadOffset { get; init; }
|
||||
|
||||
/// <summary>Gets the interacted payload envelope, or <see cref="ReadOnlySpan{T}.Empty"/> if none.</summary>
|
||||
public ReadOnlySpan<byte> InteractedPayloadEnvelope { get; init; }
|
||||
|
||||
/// <summary>Gets the interacted payload, or <c>null</c> if none.</summary>
|
||||
public Payload? InteractedPayload =>
|
||||
this.lazyPayload ??=
|
||||
this.InteractedPayloadEnvelope.IsEmpty
|
||||
? default
|
||||
: SeString.Parse(this.InteractedPayloadEnvelope).Payloads.FirstOrDefault();
|
||||
}
|
||||
|
|
@ -1,693 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using BitFaster.Caching.Lru;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Config;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.Internal.ImGuiSeStringRenderer.TextProcessing;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets2;
|
||||
using Lumina.Text.Expressions;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using static Dalamud.Game.Text.SeStringHandling.BitmapFontIcon;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
|
||||
/// <summary>Draws SeString.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class SeStringRenderer : IInternalDisposableService
|
||||
{
|
||||
private const int ChannelShadow = 0;
|
||||
private const int ChannelEdge = 1;
|
||||
private const int ChannelFore = 2;
|
||||
private const int ChannelCount = 3;
|
||||
|
||||
private const char SoftHyphen = '\u00AD';
|
||||
private const char ObjectReplacementCharacter = '\uFFFC';
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfig = Service<GameConfig>.Get();
|
||||
|
||||
private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024);
|
||||
|
||||
private readonly GfdFile gfd;
|
||||
private readonly uint[] colorTypes;
|
||||
private readonly uint[] edgeColorTypes;
|
||||
|
||||
private readonly List<TextFragment> words = [];
|
||||
|
||||
private readonly List<uint> colorStack = [];
|
||||
private readonly List<uint> edgeColorStack = [];
|
||||
private readonly List<uint> shadowColorStack = [];
|
||||
private bool bold;
|
||||
private bool italic;
|
||||
private Vector2 edge;
|
||||
private Vector2 shadow;
|
||||
|
||||
private ImDrawListSplitterPtr splitter = new(ImGuiNative.ImDrawListSplitter_ImDrawListSplitter());
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private SeStringRenderer(DataManager dm)
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.colorTypes[row.RowId] = BgraToRgba((row.UIForeground >> 8) | (row.UIForeground << 24));
|
||||
this.edgeColorTypes[row.RowId] = BgraToRgba((row.UIGlow >> 8) | (row.UIGlow << 24));
|
||||
}
|
||||
|
||||
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
|
||||
|
||||
return;
|
||||
|
||||
static uint BgraToRgba(uint x)
|
||||
{
|
||||
var buf = (byte*)&x;
|
||||
(buf[0], buf[2]) = (buf[2], buf[0]);
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
|
||||
~SeStringRenderer() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <summary>Creates and caches a SeString from a text macro representation, and then draws it.</summary>
|
||||
/// <param name="text">SeString text macro representation.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
public void CompileAndDrawWrapped(string text, float wrapWidth = 0)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
this.DrawWrapped(
|
||||
this.cache.GetOrAdd(
|
||||
text,
|
||||
static text =>
|
||||
{
|
||||
var outstr = default(Utf8String);
|
||||
outstr.Ctor();
|
||||
RaptureTextModule.Instance()->MacroEncoder.EncodeString(&outstr, text.ReplaceLineEndings("<br>"));
|
||||
var res = new ReadOnlySeString(outstr.AsSpan().ToArray());
|
||||
outstr.Dtor();
|
||||
return res;
|
||||
}).AsSpan(),
|
||||
wrapWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="DrawWrapped(ReadOnlySeStringSpan, float)"/>
|
||||
public void DrawWrapped(in Utf8String utf8String, float wrapWidth = 0) =>
|
||||
this.DrawWrapped(utf8String.AsSpan(), wrapWidth);
|
||||
|
||||
/// <summary>Draws a SeString.</summary>
|
||||
/// <param name="sss">SeString to draw.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
public void DrawWrapped(ReadOnlySeStringSpan sss, float wrapWidth = 0)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (wrapWidth <= 0)
|
||||
wrapWidth = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
this.words.Clear();
|
||||
this.colorStack.Clear();
|
||||
this.edgeColorStack.Clear();
|
||||
this.shadowColorStack.Clear();
|
||||
|
||||
this.colorStack.Add(ImGui.GetColorU32(ImGuiCol.Text));
|
||||
this.edgeColorStack.Add(0);
|
||||
this.shadowColorStack.Add(0);
|
||||
this.bold = this.italic = false;
|
||||
this.edge = Vector2.One;
|
||||
this.shadow = Vector2.Zero;
|
||||
|
||||
var state = new DrawState(
|
||||
sss,
|
||||
ImGui.GetWindowDrawList(),
|
||||
this.splitter,
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
ImGui.GetCursorScreenPos());
|
||||
this.CreateTextFragments(ref state, wrapWidth);
|
||||
|
||||
var size = Vector2.Zero;
|
||||
for (var i = 0; i < this.words.Count; i++)
|
||||
{
|
||||
var word = this.words[i];
|
||||
this.DrawWord(
|
||||
ref state,
|
||||
word.Offset,
|
||||
state.Raw.Data[word.From..word.To],
|
||||
i == 0
|
||||
? '\0'
|
||||
: this.words[i - 1].IsSoftHyphenVisible
|
||||
? this.words[i - 1].LastRuneRepr
|
||||
: this.words[i - 1].LastRuneRepr2);
|
||||
|
||||
if (word.IsSoftHyphenVisible && i > 0)
|
||||
{
|
||||
this.DrawWord(
|
||||
ref state,
|
||||
word.Offset + new Vector2(word.AdvanceWidthWithoutLastRune, 0),
|
||||
"-"u8,
|
||||
this.words[i - 1].LastRuneRepr);
|
||||
}
|
||||
|
||||
size = Vector2.Max(size, word.Offset + new Vector2(word.VisibleWidth, state.FontSize));
|
||||
}
|
||||
|
||||
state.Splitter.Merge(state.DrawList);
|
||||
|
||||
ImGui.Dummy(size);
|
||||
}
|
||||
|
||||
/// <summary>Gets the printable char for the given char, or null(\0) if it should not be handled at all.</summary>
|
||||
/// <param name="c">Character to determine.</param>
|
||||
/// <returns>Character to print, or null(\0) if none.</returns>
|
||||
private static Rune? ToPrintableRune(int c) => c switch
|
||||
{
|
||||
char.MaxValue => null,
|
||||
SoftHyphen => new('-'),
|
||||
_ when UnicodeData.LineBreak[c]
|
||||
is UnicodeLineBreakClass.BK
|
||||
or UnicodeLineBreakClass.CR
|
||||
or UnicodeLineBreakClass.LF
|
||||
or UnicodeLineBreakClass.NL => new(0),
|
||||
_ => new(c),
|
||||
};
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (this.splitter.NativePtr is not null)
|
||||
this.splitter.Destroy();
|
||||
this.splitter = default;
|
||||
}
|
||||
|
||||
private void CreateTextFragments(ref DrawState state, float wrapWidth)
|
||||
{
|
||||
var prev = 0;
|
||||
var runningOffset = Vector2.Zero;
|
||||
var runningWidth = 0f;
|
||||
foreach (var (curr, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
var fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset);
|
||||
var nextRunningWidth = Math.Max(runningWidth, runningOffset.X + fragment.VisibleWidth);
|
||||
if (nextRunningWidth <= wrapWidth)
|
||||
{
|
||||
// New fragment fits in the current line.
|
||||
if (this.words.Count > 0)
|
||||
{
|
||||
char lastFragmentEnd;
|
||||
if (this.words[^1].EndsWithSoftHyphen)
|
||||
{
|
||||
runningOffset.X += this.words[^1].AdvanceWidthWithoutLastRune - this.words[^1].AdvanceWidth;
|
||||
lastFragmentEnd = this.words[^1].LastRuneRepr;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastFragmentEnd = this.words[^1].LastRuneRepr2;
|
||||
}
|
||||
|
||||
runningOffset.X += MathF.Round(
|
||||
state.Font.GetDistanceAdjustmentForPair(lastFragmentEnd, fragment.FirstRuneRepr) *
|
||||
state.FontSizeScale);
|
||||
fragment = fragment with { Offset = runningOffset };
|
||||
}
|
||||
|
||||
this.words.Add(fragment);
|
||||
runningWidth = nextRunningWidth;
|
||||
runningOffset.X += fragment.AdvanceWidth;
|
||||
prev = curr;
|
||||
}
|
||||
else if (fragment.VisibleWidth <= wrapWidth)
|
||||
{
|
||||
// New fragment does not fit in the current line, but it will fit in the next line.
|
||||
// Implicit conditions: runningWidth > 0, this.words.Count > 0
|
||||
runningWidth = fragment.VisibleWidth;
|
||||
runningOffset.X = fragment.AdvanceWidth;
|
||||
runningOffset.Y += state.FontSize;
|
||||
prev = curr;
|
||||
this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true };
|
||||
this.words.Add(fragment with { Offset = runningOffset with { X = 0 } });
|
||||
}
|
||||
else
|
||||
{
|
||||
// New fragment does not fit in the given width, and it needs to be broken down.
|
||||
while (prev < curr)
|
||||
{
|
||||
if (runningOffset.X > 0)
|
||||
{
|
||||
runningOffset.X = 0;
|
||||
runningOffset.Y += state.FontSize;
|
||||
}
|
||||
|
||||
fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset, wrapWidth);
|
||||
runningWidth = fragment.VisibleWidth;
|
||||
runningOffset.X = fragment.AdvanceWidth;
|
||||
prev = fragment.To;
|
||||
if (this.words.Count > 0)
|
||||
this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true };
|
||||
this.words.Add(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment.MandatoryBreakAfter)
|
||||
{
|
||||
runningOffset.X = runningWidth = 0;
|
||||
runningOffset.Y += state.FontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan<byte> span, char lastRuneRepr)
|
||||
{
|
||||
var gfdTextureSrv =
|
||||
(nint)UIModule.Instance()->GetRaptureAtkModule()->AtkModule.AtkFontManager.Gfd->Texture->
|
||||
D3D11ShaderResourceView;
|
||||
var x = 0f;
|
||||
var width = 0f;
|
||||
foreach (var c in UtfEnumerator.From(span, UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
if (c.IsSeStringPayload)
|
||||
{
|
||||
var enu = new ReadOnlySeStringSpan(span[c.ByteOffset..]).GetEnumerator();
|
||||
if (!enu.MoveNext())
|
||||
continue;
|
||||
|
||||
var payload = enu.Current;
|
||||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Color:
|
||||
TouchColorStack(this.colorStack, payload);
|
||||
continue;
|
||||
case MacroCode.EdgeColor:
|
||||
TouchColorStack(this.edgeColorStack, payload);
|
||||
continue;
|
||||
case MacroCode.ShadowColor:
|
||||
TouchColorStack(this.shadowColorStack, payload);
|
||||
continue;
|
||||
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
this.bold = u != 0;
|
||||
continue;
|
||||
case MacroCode.Italic when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
|
||||
this.italic = u != 0;
|
||||
continue;
|
||||
case MacroCode.Edge when payload.TryGetExpression(out var e1, out var e2) &&
|
||||
e1.TryGetInt(out var v1) && e2.TryGetInt(out var v2):
|
||||
this.edge = new(v1, v2);
|
||||
continue;
|
||||
case MacroCode.Shadow when payload.TryGetExpression(out var e1, out var e2) &&
|
||||
e1.TryGetInt(out var v1) && e2.TryGetInt(out var v2):
|
||||
this.shadow = new(v1, v2);
|
||||
continue;
|
||||
case MacroCode.ColorType:
|
||||
TouchColorTypeStack(this.colorStack, this.colorTypes, payload);
|
||||
continue;
|
||||
case MacroCode.EdgeColorType:
|
||||
TouchColorTypeStack(this.edgeColorStack, this.edgeColorTypes, payload);
|
||||
continue;
|
||||
case MacroCode.Icon:
|
||||
case MacroCode.Icon2:
|
||||
{
|
||||
if (this.GetBitmapFontIconFor(span[c.ByteOffset..]) is not (var icon and not None) ||
|
||||
!this.gfd.TryGetEntry((uint)icon, out var gfdEntry) ||
|
||||
gfdEntry.IsEmpty)
|
||||
continue;
|
||||
|
||||
var useHq = state.FontSize > 19;
|
||||
var sizeScale = (state.FontSize + 1) / gfdEntry.Height;
|
||||
state.SetCurrentChannel(ChannelFore);
|
||||
state.Draw(
|
||||
offset + new Vector2(x, 0),
|
||||
gfdTextureSrv,
|
||||
Vector2.Zero,
|
||||
gfdEntry.Size * sizeScale,
|
||||
Vector2.Zero,
|
||||
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
|
||||
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1);
|
||||
width = Math.Max(width, x + (gfdEntry.Width * sizeScale));
|
||||
x += MathF.Round(gfdEntry.Width * sizeScale);
|
||||
lastRuneRepr = '\0';
|
||||
continue;
|
||||
}
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ToPrintableRune(c.EffectiveChar) is not { } rune)
|
||||
continue;
|
||||
|
||||
var runeRepr = rune.Value is >= 0 and < char.MaxValue ? (char)rune.Value : '\uFFFE';
|
||||
if (runeRepr != 0)
|
||||
{
|
||||
var dist = state.Font.GetDistanceAdjustmentForPair(lastRuneRepr, runeRepr);
|
||||
ref var g = ref *(ImGuiHelpers.ImFontGlyphReal*)state.Font.FindGlyph(runeRepr).NativePtr;
|
||||
|
||||
var dyItalic = this.italic
|
||||
? new Vector2(state.Font.FontSize - g.Y0, state.Font.FontSize - g.Y1) / 6
|
||||
: Vector2.Zero;
|
||||
|
||||
if (this.shadow != Vector2.Zero && this.shadowColorStack[^1] >= 0x1000000)
|
||||
{
|
||||
state.SetCurrentChannel(ChannelShadow);
|
||||
state.Draw(
|
||||
offset + this.shadow + new Vector2(x + dist, 0),
|
||||
g,
|
||||
dyItalic,
|
||||
this.shadowColorStack[^1]);
|
||||
}
|
||||
|
||||
if (this.edge != Vector2.Zero && this.edgeColorStack[^1] >= 0x1000000)
|
||||
{
|
||||
state.SetCurrentChannel(ChannelEdge);
|
||||
for (var dx = -this.edge.X; dx <= this.edge.X; dx++)
|
||||
{
|
||||
for (var dy = -this.edge.Y; dy <= this.edge.Y; dy++)
|
||||
{
|
||||
if (dx == 0 && dy == 0)
|
||||
continue;
|
||||
|
||||
state.Draw(offset + new Vector2(x + dist + dx, dy), g, dyItalic, this.edgeColorStack[^1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.SetCurrentChannel(ChannelFore);
|
||||
for (var dx = this.bold ? 1 : 0; dx >= 0; dx--)
|
||||
state.Draw(offset + new Vector2(x + dist + dx, 0), g, dyItalic, this.colorStack[^1]);
|
||||
|
||||
width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale));
|
||||
x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale);
|
||||
}
|
||||
|
||||
lastRuneRepr = runeRepr;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static void TouchColorStack(List<uint> stack, ReadOnlySePayloadSpan payload)
|
||||
{
|
||||
if (!payload.TryGetExpression(out var expr))
|
||||
return;
|
||||
if (expr.TryGetPlaceholderExpression(out var p) && p == (int)ExpressionType.StackColor && stack.Count > 1)
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
else if (expr.TryGetUInt(out var u))
|
||||
stack.Add(u);
|
||||
}
|
||||
|
||||
static void TouchColorTypeStack(List<uint> stack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
|
||||
{
|
||||
if (!payload.TryGetExpression(out var expr))
|
||||
return;
|
||||
if (!expr.TryGetUInt(out var u))
|
||||
return;
|
||||
if (u != 0)
|
||||
stack.Add(u < colorTypes.Length ? colorTypes[u] : 0u);
|
||||
else if (stack.Count > 1)
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
|
||||
{
|
||||
var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
|
||||
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
|
||||
return None;
|
||||
|
||||
var payload = e.Current;
|
||||
switch (payload.MacroCode)
|
||||
{
|
||||
case MacroCode.Icon
|
||||
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
|
||||
return (BitmapFontIcon)iconId;
|
||||
case MacroCode.Icon2
|
||||
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
|
||||
var configName = (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerShoulderLeft => SystemConfigOption.PadButton_L1,
|
||||
ControllerShoulderRight => SystemConfigOption.PadButton_R1,
|
||||
ControllerTriggerLeft => SystemConfigOption.PadButton_L2,
|
||||
ControllerTriggerRight => SystemConfigOption.PadButton_R2,
|
||||
ControllerButton3 => SystemConfigOption.PadButton_Triangle,
|
||||
ControllerButton1 => SystemConfigOption.PadButton_Cross,
|
||||
ControllerButton0 => SystemConfigOption.PadButton_Circle,
|
||||
ControllerButton2 => SystemConfigOption.PadButton_Square,
|
||||
ControllerStart => SystemConfigOption.PadButton_Start,
|
||||
ControllerBack => SystemConfigOption.PadButton_Select,
|
||||
ControllerAnalogLeftStick => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickIn => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickUpDown => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogLeftStickLeftRight => SystemConfigOption.PadButton_LS,
|
||||
ControllerAnalogRightStick => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickIn => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickUpDown => SystemConfigOption.PadButton_RS,
|
||||
ControllerAnalogRightStickLeftRight => SystemConfigOption.PadButton_RS,
|
||||
_ => (SystemConfigOption?)null,
|
||||
};
|
||||
|
||||
if (configName is null || !this.gameConfig.TryGet(configName.Value, out PadButtonValue pb))
|
||||
return (BitmapFontIcon)iconId;
|
||||
|
||||
return pb switch
|
||||
{
|
||||
PadButtonValue.Autorun_Support => ControllerShoulderLeft,
|
||||
PadButtonValue.Hotbar_Set_Change => ControllerShoulderRight,
|
||||
PadButtonValue.XHB_Left_Start => ControllerTriggerLeft,
|
||||
PadButtonValue.XHB_Right_Start => ControllerTriggerRight,
|
||||
PadButtonValue.Jump => ControllerButton3,
|
||||
PadButtonValue.Accept => ControllerButton1,
|
||||
PadButtonValue.Cancel => ControllerButton0,
|
||||
PadButtonValue.Map_Sub => ControllerButton2,
|
||||
PadButtonValue.MainCommand => ControllerStart,
|
||||
PadButtonValue.HUD_Select => ControllerBack,
|
||||
PadButtonValue.Move_Operation => (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerAnalogLeftStick => ControllerAnalogLeftStick,
|
||||
ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn,
|
||||
ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown,
|
||||
ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight,
|
||||
ControllerAnalogRightStick => ControllerAnalogLeftStick,
|
||||
ControllerAnalogRightStickIn => ControllerAnalogLeftStickIn,
|
||||
ControllerAnalogRightStickUpDown => ControllerAnalogLeftStickUpDown,
|
||||
ControllerAnalogRightStickLeftRight => ControllerAnalogLeftStickLeftRight,
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
},
|
||||
PadButtonValue.Camera_Operation => (BitmapFontIcon)iconId switch
|
||||
{
|
||||
ControllerAnalogLeftStick => ControllerAnalogRightStick,
|
||||
ControllerAnalogLeftStickIn => ControllerAnalogRightStickIn,
|
||||
ControllerAnalogLeftStickUpDown => ControllerAnalogRightStickUpDown,
|
||||
ControllerAnalogLeftStickLeftRight => ControllerAnalogRightStickLeftRight,
|
||||
ControllerAnalogRightStick => ControllerAnalogRightStick,
|
||||
ControllerAnalogRightStickIn => ControllerAnalogRightStickIn,
|
||||
ControllerAnalogRightStickUpDown => ControllerAnalogRightStickUpDown,
|
||||
ControllerAnalogRightStickLeftRight => ControllerAnalogRightStickLeftRight,
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
},
|
||||
_ => (BitmapFontIcon)iconId,
|
||||
};
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private readonly record struct TextFragment(
|
||||
int From,
|
||||
int To,
|
||||
Vector2 Offset,
|
||||
float VisibleWidth,
|
||||
float AdvanceWidth,
|
||||
float AdvanceWidthWithoutLastRune,
|
||||
bool MandatoryBreakAfter,
|
||||
bool EndsWithSoftHyphen,
|
||||
char FirstRuneRepr,
|
||||
char LastRuneRepr,
|
||||
char LastRuneRepr2)
|
||||
{
|
||||
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.MandatoryBreakAfter;
|
||||
}
|
||||
|
||||
private ref struct DrawState
|
||||
{
|
||||
public readonly ReadOnlySeStringSpan Raw;
|
||||
public readonly float FontSize;
|
||||
public readonly float FontSizeScale;
|
||||
public readonly Vector2 ScreenOffset;
|
||||
|
||||
public ImDrawListPtr DrawList;
|
||||
public ImDrawListSplitterPtr Splitter;
|
||||
public ImFontPtr Font;
|
||||
|
||||
public DrawState(
|
||||
ReadOnlySeStringSpan raw,
|
||||
ImDrawListPtr drawList,
|
||||
ImDrawListSplitterPtr splitter,
|
||||
ImFontPtr font,
|
||||
float fontSize,
|
||||
Vector2 screenOffset)
|
||||
{
|
||||
this.Raw = raw;
|
||||
this.DrawList = drawList;
|
||||
this.Splitter = splitter;
|
||||
this.Font = font;
|
||||
this.FontSize = fontSize;
|
||||
this.FontSizeScale = fontSize / font.FontSize;
|
||||
this.ScreenOffset = screenOffset;
|
||||
|
||||
splitter.Split(drawList, ChannelCount);
|
||||
}
|
||||
|
||||
public void SetCurrentChannel(int channelIndex) => this.Splitter.SetCurrentChannel(this.DrawList, channelIndex);
|
||||
|
||||
public void Draw(Vector2 offset, in ImGuiHelpers.ImFontGlyphReal g, Vector2 dyItalic, uint color) =>
|
||||
this.Draw(
|
||||
offset,
|
||||
this.Font.ContainerAtlas.Textures[g.TextureIndex].TexID,
|
||||
g.XY0 * this.FontSizeScale,
|
||||
g.XY1 * this.FontSizeScale,
|
||||
dyItalic * this.FontSizeScale,
|
||||
g.UV0,
|
||||
g.UV1,
|
||||
color);
|
||||
|
||||
public void Draw(
|
||||
Vector2 offset,
|
||||
nint igTextureId,
|
||||
Vector2 xy0,
|
||||
Vector2 xy1,
|
||||
Vector2 dyItalic,
|
||||
Vector2 uv0,
|
||||
Vector2 uv1,
|
||||
uint color = uint.MaxValue)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public TextFragment CreateFragment(
|
||||
SeStringRenderer renderer,
|
||||
int from,
|
||||
int to,
|
||||
bool mandatoryBreakAfter,
|
||||
Vector2 offset,
|
||||
float wrapWidth = float.PositiveInfinity)
|
||||
{
|
||||
var lastNonSpace = from;
|
||||
|
||||
var x = 0f;
|
||||
var w = 0f;
|
||||
var visibleWidth = 0f;
|
||||
var advanceWidth = 0f;
|
||||
var prevAdvanceWidth = 0f;
|
||||
var firstRuneRepr = char.MaxValue;
|
||||
var lastRuneRepr = default(char);
|
||||
var lastRuneRepr2 = default(char);
|
||||
var endsWithSoftHyphen = false;
|
||||
foreach (var c in UtfEnumerator.From(this.Raw.Data[from..to], UtfEnumeratorFlags.Utf8SeString))
|
||||
{
|
||||
prevAdvanceWidth = x;
|
||||
lastRuneRepr2 = lastRuneRepr;
|
||||
endsWithSoftHyphen = c.EffectiveChar == SoftHyphen;
|
||||
|
||||
var byteOffset = from + c.ByteOffset;
|
||||
var isBreakableWhitespace = false;
|
||||
if (c is { IsSeStringPayload: true, MacroCode: MacroCode.Icon or MacroCode.Icon2 } &&
|
||||
renderer.GetBitmapFontIconFor(this.Raw.Data[byteOffset..]) is var icon and not None &&
|
||||
renderer.gfd.TryGetEntry((uint)icon, out var gfdEntry) &&
|
||||
!gfdEntry.IsEmpty)
|
||||
{
|
||||
var sizeScale = (this.FontSize + 1) / gfdEntry.Height;
|
||||
w = Math.Max(w, x + (gfdEntry.Width * sizeScale));
|
||||
x += MathF.Round(gfdEntry.Width * sizeScale);
|
||||
lastRuneRepr = default;
|
||||
}
|
||||
else if (ToPrintableRune(c.EffectiveChar) is { } rune)
|
||||
{
|
||||
var runeRepr = rune.Value is >= 0 and < char.MaxValue ? (char)rune.Value : '\uFFFE';
|
||||
if (runeRepr != 0)
|
||||
{
|
||||
var dist = this.Font.GetDistanceAdjustmentForPair(lastRuneRepr, runeRepr);
|
||||
ref var g = ref *(ImGuiHelpers.ImFontGlyphReal*)this.Font.FindGlyph(runeRepr).NativePtr;
|
||||
w = Math.Max(w, x + ((dist + g.X1) * this.FontSizeScale));
|
||||
x += MathF.Round((dist + g.AdvanceX) * this.FontSizeScale);
|
||||
}
|
||||
|
||||
isBreakableWhitespace = Rune.IsWhiteSpace(rune) &&
|
||||
UnicodeData.LineBreak[rune.Value] is not UnicodeLineBreakClass.GL;
|
||||
lastRuneRepr = runeRepr;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstRuneRepr == char.MaxValue)
|
||||
firstRuneRepr = lastRuneRepr;
|
||||
|
||||
if (isBreakableWhitespace)
|
||||
{
|
||||
advanceWidth = x;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (w > wrapWidth && lastNonSpace != from && !endsWithSoftHyphen)
|
||||
{
|
||||
to = byteOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
advanceWidth = x;
|
||||
visibleWidth = w;
|
||||
lastNonSpace = byteOffset + c.ByteLength;
|
||||
}
|
||||
}
|
||||
|
||||
return new(
|
||||
from,
|
||||
to,
|
||||
offset,
|
||||
visibleWidth,
|
||||
advanceWidth,
|
||||
prevAdvanceWidth,
|
||||
mandatoryBreakAfter,
|
||||
endsWithSoftHyphen,
|
||||
firstRuneRepr,
|
||||
lastRuneRepr,
|
||||
lastRuneRepr2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
using System.Numerics;
|
||||
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Internal.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
// Customised version of https://github.com/aers/FFXIVUIDebug
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
|
@ -204,10 +209,22 @@ internal unsafe class UiDebug
|
|||
var textNode = (AtkTextNode*)node;
|
||||
ImGui.Text("text: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textNode->NodeText);
|
||||
Service<SeStringRenderer>.Get().Draw(textNode->NodeText);
|
||||
|
||||
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button($"Encode##{(ulong)textNode:X}"))
|
||||
{
|
||||
using var tmp = new Utf8String();
|
||||
RaptureTextModule.Instance()->MacroEncoder.EncodeString(&tmp, textNode->NodeText.StringPtr);
|
||||
textNode->NodeText.Copy(&tmp);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button($"Decode##{(ulong)textNode:X}"))
|
||||
textNode->NodeText.SetString(new ReadOnlySeStringSpan(textNode->NodeText.StringPtr).ToString());
|
||||
|
||||
ImGui.Text($"AlignmentType: {(AlignmentType)textNode->AlignmentFontType} FontSize: {textNode->FontSize}");
|
||||
int b = textNode->AlignmentFontType;
|
||||
if (ImGui.InputInt($"###setAlignment{(ulong)textNode:X}", ref b, 1))
|
||||
|
|
@ -233,7 +250,7 @@ internal unsafe class UiDebug
|
|||
var counterNode = (AtkCounterNode*)node;
|
||||
ImGui.Text("text: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(counterNode->NodeText);
|
||||
Service<SeStringRenderer>.Get().Draw(counterNode->NodeText);
|
||||
break;
|
||||
case NodeType.Image:
|
||||
var imageNode = (AtkImageNode*)node;
|
||||
|
|
@ -372,31 +389,31 @@ internal unsafe class UiDebug
|
|||
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
|
||||
ImGui.Text("InputBase Text1: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText1);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText1);
|
||||
|
||||
ImGui.Text("InputBase Text2: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->AtkComponentInputBase.UnkText2);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText2);
|
||||
|
||||
ImGui.Text("Text1: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText01);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText01);
|
||||
|
||||
ImGui.Text("Text2: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText02);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02);
|
||||
|
||||
ImGui.Text("Text3: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText03);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText03);
|
||||
|
||||
ImGui.Text("Text4: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText04);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText04);
|
||||
|
||||
ImGui.Text("Text5: ");
|
||||
ImGui.SameLine();
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(textInputComponent->UnkText05);
|
||||
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText05);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,9 @@ using System.Text.Unicode;
|
|||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
|
@ -16,10 +19,6 @@ using Dalamud.Interface.Utility.Raii;
|
|||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using SeStringRenderer = Dalamud.Interface.Internal.ImGuiSeStringRenderer.SeStringRenderer;
|
||||
|
||||
namespace Dalamud.Interface.Utility;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -181,13 +180,57 @@ public static class ImGuiHelpers
|
|||
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}");
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SeStringRenderer.DrawWrapped(ReadOnlySeStringSpan, float)"/>
|
||||
/// <summary>Draws a SeString.</summary>
|
||||
/// <param name="sss">SeString to draw.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
/// <remarks>This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
|
||||
/// The function definition is stable; only in the next API version a function may be removed.</remarks>
|
||||
public static void SeStringWrapped(ReadOnlySpan<byte> sss, float wrapWidth = 0) =>
|
||||
Service<SeStringRenderer>.Get().DrawWrapped(sss, wrapWidth);
|
||||
Service<SeStringRenderer>.Get().Draw(sss, new() { WrapWidth = wrapWidth > 0 ? wrapWidth : null });
|
||||
|
||||
/// <inheritdoc cref="SeStringRenderer.CompileAndDrawWrapped"/>
|
||||
/// <summary>Creates and caches a SeString from a text macro representation, and then draws it.</summary>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to <see cref="NewLinePayload"/>.</param>
|
||||
/// <param name="wrapWidth">Wrapping width. If a non-positive number is provided, then the remainder of the width
|
||||
/// will be used.</param>
|
||||
/// <remarks>This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
|
||||
/// The function definition is stable; only in the next API version a function may be removed.</remarks>
|
||||
public static void CompileSeStringWrapped(string text, float wrapWidth = 0) =>
|
||||
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(text, wrapWidth);
|
||||
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(
|
||||
text,
|
||||
new() { WrapWidth = wrapWidth > 0 ? wrapWidth : null });
|
||||
|
||||
/// <summary>Draws a SeString.</summary>
|
||||
/// <param name="sss">SeString to draw.</param>
|
||||
/// <param name="style">Initial rendering style.</param>
|
||||
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
|
||||
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
|
||||
/// <returns>Interaction result of the rendered text.</returns>
|
||||
/// <remarks>This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
|
||||
/// The function definition is stable; only in the next API version a function may be removed.</remarks>
|
||||
public static SeStringDrawResult SeStringWrapped(
|
||||
ReadOnlySpan<byte> sss,
|
||||
in SeStringDrawParams style = default,
|
||||
ImGuiId imGuiId = default,
|
||||
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
|
||||
Service<SeStringRenderer>.Get().Draw(sss, style, imGuiId, buttonFlags);
|
||||
|
||||
/// <summary>Creates and caches a SeString from a text macro representation, and then draws it.</summary>
|
||||
/// <param name="text">SeString text macro representation.
|
||||
/// Newline characters will be normalized to <see cref="NewLinePayload"/>.</param>
|
||||
/// <param name="style">Initial rendering style.</param>
|
||||
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
|
||||
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
|
||||
/// <returns>Interaction result of the rendered text.</returns>
|
||||
/// <remarks>This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
|
||||
/// The function definition is stable; only in the next API version a function may be removed.</remarks>
|
||||
public static SeStringDrawResult CompileSeStringWrapped(
|
||||
string text,
|
||||
in SeStringDrawParams style,
|
||||
ImGuiId imGuiId = default,
|
||||
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
|
||||
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
|
||||
|
||||
/// <summary>
|
||||
/// Write unformatted text wrapped.
|
||||
|
|
|
|||
176
Dalamud/Interface/Utility/ImGuiId.cs
Normal file
176
Dalamud/Interface/Utility/ImGuiId.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Utility;
|
||||
|
||||
/// <summary>Represents any type of ImGui ID.</summary>
|
||||
public readonly ref struct ImGuiId
|
||||
{
|
||||
/// <summary>Type of the ID.</summary>
|
||||
public readonly Type IdType;
|
||||
|
||||
/// <summary>Numeric ID. Valid if <see cref="IdType"/> is <see cref="Type.Numeric"/>.</summary>
|
||||
public readonly nint Numeric;
|
||||
|
||||
/// <summary>UTF-16 string ID. Valid if <see cref="IdType"/> is <see cref="Type.U16"/>.</summary>
|
||||
public readonly ReadOnlySpan<char> U16;
|
||||
|
||||
/// <summary>UTF-8 string ID. Valid if <see cref="IdType"/> is <see cref="Type.U8"/>.</summary>
|
||||
public readonly ReadOnlySpan<byte> U8;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ImGuiId"/> struct.</summary>
|
||||
/// <param name="id">A numeric ID, or 0 to not provide an ID.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ImGuiId(nint id)
|
||||
{
|
||||
if (id != 0)
|
||||
(this.IdType, this.Numeric) = (Type.Numeric, id);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ImGuiId"/> struct.</summary>
|
||||
/// <param name="id">A UTF-16 string ID, or <see cref="ReadOnlySpan{T}.Empty"/> to not provide an ID.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ImGuiId(ReadOnlySpan<char> id)
|
||||
{
|
||||
if (!id.IsEmpty)
|
||||
{
|
||||
this.IdType = Type.U16;
|
||||
this.U16 = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ImGuiId"/> struct.</summary>
|
||||
/// <param name="id">A UTF-8 string ID, or <see cref="ReadOnlySpan{T}.Empty"/> to not provide an ID.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ImGuiId(ReadOnlySpan<byte> id)
|
||||
{
|
||||
if (!id.IsEmpty)
|
||||
{
|
||||
this.IdType = Type.U8;
|
||||
this.U8 = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Possible types for an ImGui ID.</summary>
|
||||
public enum Type
|
||||
{
|
||||
/// <summary>No ID is specified.</summary>
|
||||
None,
|
||||
|
||||
/// <summary><see cref="ImGuiId.Numeric"/> field is used.</summary>
|
||||
Numeric,
|
||||
|
||||
/// <summary><see cref="ImGuiId.U16"/> field is used.</summary>
|
||||
U16,
|
||||
|
||||
/// <summary><see cref="ImGuiId.U8"/> field is used.</summary>
|
||||
U8,
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe implicit operator ImGuiId(void* id) => new((nint)id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe implicit operator ImGuiId(float id) => new(*(int*)&id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe implicit operator ImGuiId(double id) => new(*(nint*)&id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(sbyte id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(byte id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(char id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(short id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(ushort id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(int id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(uint id) => new((nint)id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(nint id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(nuint id) => new((nint)id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(Span<char> id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(ReadOnlySpan<char> id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(Memory<char> id) => new(id.Span);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(ReadOnlyMemory<char> id) => new(id.Span);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(char[] id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(string id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(Span<byte> id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(ReadOnlySpan<byte> id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(Memory<byte> id) => new(id.Span);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(ReadOnlyMemory<byte> id) => new(id.Span);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ImGuiId(byte[] id) => new(id);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator bool(ImGuiId id) => !id.IsEmpty();
|
||||
|
||||
/// <summary>Determines if no ID is stored.</summary>
|
||||
/// <returns><c>true</c> if no ID is stored.</returns>
|
||||
public bool IsEmpty() => this.IdType switch
|
||||
{
|
||||
Type.None => true,
|
||||
Type.Numeric => this.Numeric == 0,
|
||||
Type.U16 => this.U16.IsEmpty,
|
||||
Type.U8 => this.U8.IsEmpty,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// <summary>Pushes ID if any is stored.</summary>
|
||||
/// <returns><c>true</c> if any ID is pushed.</returns>
|
||||
public unsafe bool PushId()
|
||||
{
|
||||
switch (this.IdType)
|
||||
{
|
||||
case Type.Numeric:
|
||||
ImGuiNative.igPushID_Ptr((void*)this.Numeric);
|
||||
return true;
|
||||
case Type.U16:
|
||||
fixed (void* p = this.U16)
|
||||
ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + (this.U16.Length * 2));
|
||||
return true;
|
||||
case Type.U8:
|
||||
fixed (void* p = this.U8)
|
||||
ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + this.U8.Length);
|
||||
return true;
|
||||
case Type.None:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
|
||||
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||
using DSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
||||
using LSeString = Lumina.Text.SeString;
|
||||
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
|
|
@ -13,7 +18,51 @@ public static class SeStringExtensions
|
|||
/// </summary>
|
||||
/// <param name="originalString">The original Lumina SeString.</param>
|
||||
/// <returns>The re-parsed Dalamud SeString.</returns>
|
||||
public static SeString ToDalamudString(this Lumina.Text.SeString originalString) => SeString.Parse(originalString.RawData);
|
||||
public static DSeString ToDalamudString(this LSeString originalString) => DSeString.Parse(originalString.RawData);
|
||||
|
||||
/// <summary>Compiles and appends a macro string.</summary>
|
||||
/// <param name="ssb">Target SeString builder.</param>
|
||||
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
/// <remarks>Must be called from the main thread.</remarks>
|
||||
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return ssb.Append(Service<SeStringRenderer>.Get().Compile(macroString));
|
||||
}
|
||||
|
||||
/// <summary>Compiles and appends a macro string.</summary>
|
||||
/// <param name="ssb">Target SeString builder.</param>
|
||||
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
/// <remarks>Must be called from the main thread.</remarks>
|
||||
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return ssb.Append(Service<SeStringRenderer>.Get().Compile(macroString));
|
||||
}
|
||||
|
||||
/// <summary>Compiles and appends a macro string.</summary>
|
||||
/// <param name="ssb">Target SeString builder.</param>
|
||||
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
/// <remarks>Must be called from the main thread.</remarks>
|
||||
public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan<byte> macroString)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return ssb.Append(DSeString.Parse(Service<SeStringRenderer>.Get().Compile(macroString)));
|
||||
}
|
||||
|
||||
/// <summary>Compiles and appends a macro string.</summary>
|
||||
/// <param name="ssb">Target SeString builder.</param>
|
||||
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
/// <remarks>Must be called from the main thread.</remarks>
|
||||
public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan<char> macroString)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return ssb.Append(DSeString.Parse(Service<SeStringRenderer>.Get().Compile(macroString)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate if character name is valid.
|
||||
|
|
@ -24,8 +73,5 @@ public static class SeStringExtensions
|
|||
/// </summary>
|
||||
/// <param name="value">character name to validate.</param>
|
||||
/// <returns>indicator if character is name is valid.</returns>
|
||||
public static bool IsValidCharacterName(this SeString value)
|
||||
{
|
||||
return value.ToString().IsValidCharacterName();
|
||||
}
|
||||
public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue