810 lines
26 KiB
C#
810 lines
26 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.ImGuiSeStringRenderer;
|
|
using Dalamud.Interface.Textures.TextureWraps;
|
|
using Dalamud.Interface.Utility;
|
|
using Lumina.Text.Parse;
|
|
using Lumina.Text.ReadOnly;
|
|
using System.Globalization;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
|
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
|
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
|
|
|
|
namespace LightlessSync.Utils;
|
|
|
|
public static class SeStringUtils
|
|
{
|
|
private static int _seStringHitboxCounter;
|
|
private static int _iconHitboxCounter;
|
|
|
|
public static bool TryRenderSeStringMarkupAtCursor(string payload)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(payload))
|
|
return false;
|
|
|
|
var wrapWidth = ImGui.GetContentRegionAvail().X;
|
|
if (wrapWidth <= 0f || float.IsNaN(wrapWidth) || float.IsInfinity(wrapWidth))
|
|
wrapWidth = float.MaxValue;
|
|
|
|
var normalizedPayload = payload.ReplaceLineEndings("<br>");
|
|
try
|
|
{
|
|
_ = ReadOnlySeString.FromMacroString(normalizedPayload, new MacroStringParseOptions
|
|
{
|
|
ExceptionMode = MacroStringParseExceptionMode.Throw
|
|
});
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
WrapWidth = wrapWidth,
|
|
Font = ImGui.GetFont(),
|
|
Color = ImGui.GetColorU32(ImGuiCol.Text),
|
|
};
|
|
|
|
var renderId = ImGui.GetID($"SeStringMarkup##{normalizedPayload.GetHashCode(StringComparison.Ordinal)}");
|
|
var drawResult = ImGuiHelpers.CompileSeStringWrapped(normalizedPayload, drawParams, renderId);
|
|
var height = drawResult.Size.Y;
|
|
if (height <= 0f)
|
|
height = ImGui.GetTextLineHeight();
|
|
|
|
ImGui.Dummy(new Vector2(0f, height));
|
|
|
|
if (drawResult.InteractedPayloadEnvelope.Length > 0 &&
|
|
TryExtractLink(drawResult.InteractedPayloadEnvelope, drawResult.InteractedPayload, out var linkUrl, out var tooltipText))
|
|
{
|
|
var hoverText = !string.IsNullOrEmpty(linkUrl) ? linkUrl : tooltipText;
|
|
|
|
if (!string.IsNullOrEmpty(hoverText))
|
|
{
|
|
ImGui.BeginTooltip();
|
|
ImGui.TextUnformatted(hoverText);
|
|
ImGui.EndTooltip();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(linkUrl))
|
|
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
|
|
|
|
if (drawResult.Clicked && !string.IsNullOrEmpty(linkUrl))
|
|
Dalamud.Utility.Util.OpenLink(linkUrl);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ImGui.TextDisabled($"[SeString error] {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public enum SeStringSegmentType
|
|
{
|
|
Text,
|
|
Icon
|
|
}
|
|
|
|
public readonly record struct SeStringSegment(
|
|
SeStringSegmentType Type,
|
|
string? Text,
|
|
Vector4? Color,
|
|
uint IconId,
|
|
IDalamudTextureWrap? Texture,
|
|
Vector2 Size);
|
|
|
|
public static bool TryResolveSegments(
|
|
string payload,
|
|
float scale,
|
|
Func<uint, IDalamudTextureWrap?> iconResolver,
|
|
List<SeStringSegment> resolvedSegments,
|
|
out Vector2 totalSize)
|
|
{
|
|
totalSize = Vector2.Zero;
|
|
if (string.IsNullOrWhiteSpace(payload))
|
|
return false;
|
|
|
|
var parsedSegments = new List<ParsedSegment>(Math.Max(1, payload.Length / 4));
|
|
if (!ParseSegments(payload, parsedSegments))
|
|
return false;
|
|
|
|
float width = 0f;
|
|
float height = 0f;
|
|
|
|
foreach (var segment in parsedSegments)
|
|
{
|
|
switch (segment.Type)
|
|
{
|
|
case ParsedSegmentType.Text:
|
|
{
|
|
var text = segment.Text ?? string.Empty;
|
|
if (text.Length == 0)
|
|
break;
|
|
|
|
var textSize = ImGui.CalcTextSize(text);
|
|
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Text, text, segment.Color, 0, null, textSize));
|
|
width += textSize.X;
|
|
height = MathF.Max(height, textSize.Y);
|
|
break;
|
|
}
|
|
case ParsedSegmentType.Icon:
|
|
{
|
|
var wrap = iconResolver(segment.IconId);
|
|
Vector2 iconSize;
|
|
string? fallback = null;
|
|
if (wrap != null)
|
|
{
|
|
iconSize = CalculateIconSize(wrap, scale);
|
|
}
|
|
else
|
|
{
|
|
fallback = $"[{segment.IconId}]";
|
|
iconSize = ImGui.CalcTextSize(fallback);
|
|
}
|
|
|
|
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Icon, fallback, segment.Color, segment.IconId, wrap, iconSize));
|
|
width += iconSize.X;
|
|
height = MathF.Max(height, iconSize.Y);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
totalSize = new Vector2(width, height);
|
|
parsedSegments.Clear();
|
|
return resolvedSegments.Count > 0;
|
|
}
|
|
|
|
private enum ParsedSegmentType
|
|
{
|
|
Text,
|
|
Icon
|
|
}
|
|
|
|
private readonly record struct ParsedSegment(
|
|
ParsedSegmentType Type,
|
|
string? Text,
|
|
uint IconId,
|
|
Vector4? Color);
|
|
|
|
private static bool ParseSegments(string payload, List<ParsedSegment> segments)
|
|
{
|
|
var builder = new StringBuilder(payload.Length);
|
|
Vector4? activeColor = null;
|
|
var index = 0;
|
|
while (index < payload.Length)
|
|
{
|
|
if (payload[index] == '<')
|
|
{
|
|
var end = payload.IndexOf('>', index);
|
|
if (end == -1)
|
|
break;
|
|
|
|
var tagContent = payload.Substring(index + 1, end - index - 1);
|
|
if (TryHandleIconTag(tagContent, segments, builder, activeColor))
|
|
{
|
|
index = end + 1;
|
|
continue;
|
|
}
|
|
|
|
if (TryHandleColorTag(tagContent, segments, builder, ref activeColor))
|
|
{
|
|
index = end + 1;
|
|
continue;
|
|
}
|
|
|
|
builder.Append('<');
|
|
builder.Append(tagContent);
|
|
builder.Append('>');
|
|
index = end + 1;
|
|
}
|
|
else
|
|
{
|
|
builder.Append(payload[index]);
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (index < payload.Length)
|
|
builder.Append(payload, index, payload.Length - index);
|
|
|
|
FlushTextBuilder(builder, activeColor, segments);
|
|
return segments.Count > 0;
|
|
}
|
|
|
|
private static bool TryHandleIconTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, Vector4? activeColor)
|
|
{
|
|
if (!tagContent.StartsWith("icon(", StringComparison.OrdinalIgnoreCase) || !tagContent.EndsWith(')'))
|
|
return false;
|
|
|
|
var inner = tagContent.AsSpan(5, tagContent.Length - 6).Trim();
|
|
if (!uint.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var iconId))
|
|
return false;
|
|
|
|
FlushTextBuilder(textBuilder, activeColor, segments);
|
|
segments.Add(new ParsedSegment(ParsedSegmentType.Icon, null, iconId, null));
|
|
return true;
|
|
}
|
|
|
|
private static bool TryHandleColorTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, ref Vector4? activeColor)
|
|
{
|
|
if (tagContent.StartsWith("color", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var equalsIndex = tagContent.IndexOf('=');
|
|
if (equalsIndex == -1)
|
|
return false;
|
|
|
|
var value = tagContent.Substring(equalsIndex + 1).Trim().Trim('"');
|
|
if (!TryParseColor(value, out var color))
|
|
return false;
|
|
|
|
FlushTextBuilder(textBuilder, activeColor, segments);
|
|
activeColor = color;
|
|
return true;
|
|
}
|
|
|
|
if (tagContent.Equals("/color", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
FlushTextBuilder(textBuilder, activeColor, segments);
|
|
activeColor = null;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void FlushTextBuilder(StringBuilder builder, Vector4? color, List<ParsedSegment> segments)
|
|
{
|
|
if (builder.Length == 0)
|
|
return;
|
|
|
|
segments.Add(new ParsedSegment(ParsedSegmentType.Text, builder.ToString(), 0, color));
|
|
builder.Clear();
|
|
}
|
|
|
|
private static bool TryExtractLink(ReadOnlySpan<byte> envelope, Payload? payload, out string url, out string? tooltipText)
|
|
{
|
|
url = string.Empty;
|
|
tooltipText = null;
|
|
|
|
if (envelope.Length == 0 && payload is null)
|
|
return false;
|
|
|
|
tooltipText = envelope.Length > 0 ? DalamudSeString.Parse(envelope.ToArray()).TextValue : null;
|
|
|
|
if (payload is not null && TryReadUrlFromPayload(payload, out url))
|
|
return true;
|
|
|
|
if (!string.IsNullOrWhiteSpace(tooltipText))
|
|
{
|
|
var candidate = FindFirstUrl(tooltipText);
|
|
if (!string.IsNullOrEmpty(candidate))
|
|
{
|
|
url = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
url = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryReadUrlFromPayload(object payload, out string url)
|
|
{
|
|
url = string.Empty;
|
|
var type = payload.GetType();
|
|
|
|
static string? ReadStringProperty(Type type, object instance, string propertyName)
|
|
=> type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
|
?.GetValue(instance) as string;
|
|
|
|
string? candidate = ReadStringProperty(type, payload, "Uri")
|
|
?? ReadStringProperty(type, payload, "Url")
|
|
?? ReadStringProperty(type, payload, "Target")
|
|
?? ReadStringProperty(type, payload, "Destination");
|
|
|
|
if (IsHttpUrl(candidate))
|
|
{
|
|
url = candidate!;
|
|
return true;
|
|
}
|
|
|
|
var dataProperty = type.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (dataProperty?.GetValue(payload) is IEnumerable<string> sequence)
|
|
{
|
|
foreach (var entry in sequence)
|
|
{
|
|
if (IsHttpUrl(entry))
|
|
{
|
|
url = entry;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
var extraStringProp = type.GetProperty("ExtraString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (IsHttpUrl(extraStringProp?.GetValue(payload) as string))
|
|
{
|
|
url = (extraStringProp!.GetValue(payload) as string)!;
|
|
return true;
|
|
}
|
|
|
|
var textProp = type.GetProperty("Text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (IsHttpUrl(textProp?.GetValue(payload) as string))
|
|
{
|
|
url = (textProp!.GetValue(payload) as string)!;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string? FindFirstUrl(string? text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return null;
|
|
|
|
var index = text.IndexOf("http", StringComparison.OrdinalIgnoreCase);
|
|
while (index >= 0)
|
|
{
|
|
var end = index;
|
|
while (end < text.Length && !char.IsWhiteSpace(text[end]) && !"\"')]>".Contains(text[end]))
|
|
end++;
|
|
|
|
var candidate = text.Substring(index, end - index).TrimEnd('.', ',', ';');
|
|
if (IsHttpUrl(candidate))
|
|
return candidate;
|
|
|
|
index = text.IndexOf("http", end, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool IsHttpUrl(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return false;
|
|
|
|
return Uri.TryCreate(value, UriKind.Absolute, out var uri)
|
|
&& (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) || string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal));
|
|
}
|
|
|
|
public static string StripMarkup(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return string.Empty;
|
|
|
|
var builder = new StringBuilder(value.Length);
|
|
int depth = 0;
|
|
foreach (var c in value)
|
|
{
|
|
if (c == '<')
|
|
{
|
|
depth++;
|
|
continue;
|
|
}
|
|
|
|
if (c == '>' && depth > 0)
|
|
{
|
|
depth--;
|
|
continue;
|
|
}
|
|
|
|
if (depth == 0)
|
|
builder.Append(c);
|
|
}
|
|
|
|
return builder.ToString().Trim();
|
|
}
|
|
|
|
private static bool TryParseColor(string value, out Vector4 color)
|
|
{
|
|
color = default;
|
|
if (string.IsNullOrEmpty(value) || value[0] != '#')
|
|
return false;
|
|
|
|
var hex = value.AsSpan(1);
|
|
if (!uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
|
return false;
|
|
|
|
byte a, r, g, b;
|
|
if (hex.Length == 6)
|
|
{
|
|
a = 0xFF;
|
|
r = (byte)(parsed >> 16);
|
|
g = (byte)(parsed >> 8);
|
|
b = (byte)parsed;
|
|
}
|
|
else if (hex.Length == 8)
|
|
{
|
|
a = (byte)(parsed >> 24);
|
|
r = (byte)(parsed >> 16);
|
|
g = (byte)(parsed >> 8);
|
|
b = (byte)parsed;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const float inv = 1.0f / 255f;
|
|
color = new Vector4(r * inv, g * inv, b * inv, a * inv);
|
|
return true;
|
|
}
|
|
|
|
private static Vector2 CalculateIconSize(IDalamudTextureWrap wrap, float scale)
|
|
{
|
|
const float IconHeightScale = 1.25f;
|
|
|
|
var textHeight = ImGui.GetTextLineHeight();
|
|
var baselineHeight = MathF.Max(1f, textHeight - 2f * scale);
|
|
var targetHeight = MathF.Max(baselineHeight, baselineHeight * IconHeightScale);
|
|
var aspect = wrap.Width > 0 ? wrap.Width / (float)wrap.Height : 1f;
|
|
return new Vector2(targetHeight * aspect, targetHeight);
|
|
}
|
|
|
|
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
|
{
|
|
var b = new DalamudSeStringBuilder();
|
|
|
|
if (glowColor is Vector4 glow)
|
|
b.Add(new GlowPayload(glow));
|
|
|
|
if (textColor is Vector4 color)
|
|
b.Add(new ColorPayload(color));
|
|
|
|
b.AddText(text ?? string.Empty);
|
|
|
|
if (textColor is not null)
|
|
b.Add(new ColorEndPayload());
|
|
|
|
if (glowColor is not null)
|
|
b.Add(new GlowEndPayload());
|
|
|
|
return b.Build();
|
|
}
|
|
|
|
public static DalamudSeString BuildPlain(string text)
|
|
{
|
|
var b = new DalamudSeStringBuilder();
|
|
b.AddText(text ?? string.Empty);
|
|
return b.Build();
|
|
}
|
|
|
|
public static DalamudSeString BuildRichText(ReadOnlySpan<RichTextEntry> fragments)
|
|
{
|
|
var builder = new LuminaSeStringBuilder();
|
|
|
|
foreach (var fragment in fragments)
|
|
{
|
|
if (string.IsNullOrEmpty(fragment.Text))
|
|
continue;
|
|
|
|
var hasColor = fragment.Color.HasValue;
|
|
if (hasColor)
|
|
{
|
|
Vector4 color = fragment.Color!.Value;
|
|
builder.PushColorRgba(color);
|
|
}
|
|
|
|
if (fragment.Bold)
|
|
builder.AppendSetBold(true);
|
|
|
|
builder.Append(fragment.Text.AsSpan());
|
|
|
|
if (fragment.Bold)
|
|
builder.AppendSetBold(false);
|
|
|
|
if (hasColor)
|
|
builder.PopColor();
|
|
}
|
|
|
|
return DalamudSeString.Parse(builder.ToArray());
|
|
}
|
|
|
|
public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan());
|
|
public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
|
|
{
|
|
drawList ??= ImGui.GetWindowDrawList();
|
|
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
Font = font ?? UiBuilder.MonoFont,
|
|
Color = 0xFFFFFFFF,
|
|
WrapWidth = float.MaxValue,
|
|
TargetDrawList = drawList
|
|
};
|
|
|
|
ImGui.SetCursorScreenPos(position);
|
|
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
|
|
|
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
|
if (textSize.Y <= 0f)
|
|
textSize.Y = ImGui.GetTextLineHeight();
|
|
|
|
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
|
}
|
|
|
|
public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
|
|
{
|
|
drawList ??= ImGui.GetWindowDrawList();
|
|
|
|
var usedFont = font ?? ImGui.GetFont();
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
Font = usedFont,
|
|
FontSize = usedFont.FontSize,
|
|
Color = ImGui.GetColorU32(ImGuiCol.Text),
|
|
WrapWidth = wrapWidth,
|
|
TargetDrawList = drawList,
|
|
ScreenOffset = ImGui.GetCursorScreenPos()
|
|
};
|
|
|
|
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
|
|
|
var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f;
|
|
var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth);
|
|
if (textSize.Y <= 0f)
|
|
textSize.Y = ImGui.GetTextLineHeight();
|
|
|
|
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
|
}
|
|
|
|
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null)
|
|
{
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var usedFont = font ?? UiBuilder.MonoFont;
|
|
|
|
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
|
if (textSize.Y <= 0f)
|
|
{
|
|
textSize.Y = usedFont.FontSize;
|
|
}
|
|
|
|
var style = ImGui.GetStyle();
|
|
var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
|
|
var frameHeight = fontHeight + style.FramePadding.Y * 2f;
|
|
var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
|
|
var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);
|
|
|
|
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
FontSize = usedFont.FontSize,
|
|
ScreenOffset = drawPos,
|
|
Font = usedFont,
|
|
Color = 0xFFFFFFFF,
|
|
WrapWidth = float.MaxValue,
|
|
TargetDrawList = drawList
|
|
};
|
|
|
|
ImGui.SetCursorScreenPos(drawPos);
|
|
|
|
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
|
|
|
ImGui.SetCursorScreenPos(position);
|
|
if (id is not null)
|
|
{
|
|
ImGui.PushID(id);
|
|
}
|
|
else
|
|
{
|
|
ImGui.PushID(Interlocked.Increment(ref _seStringHitboxCounter));
|
|
}
|
|
|
|
try
|
|
{
|
|
ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
|
|
}
|
|
finally
|
|
{
|
|
ImGui.PopID();
|
|
}
|
|
|
|
return new Vector2(textSize.X, hitboxHeight);
|
|
}
|
|
|
|
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, float? targetFontSize, ImFontPtr? font = null, string? id = null)
|
|
{
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var usedFont = font ?? ImGui.GetFont();
|
|
|
|
ImGui.PushFont(usedFont);
|
|
Vector2 rawSize;
|
|
float usedEffectiveSize;
|
|
try
|
|
{
|
|
usedEffectiveSize = ImGui.GetFontSize();
|
|
rawSize = ImGui.CalcTextSize(seString.TextValue);
|
|
}
|
|
finally
|
|
{
|
|
ImGui.PopFont();
|
|
}
|
|
|
|
var desiredSize = targetFontSize ?? usedEffectiveSize;
|
|
var scale = usedEffectiveSize > 0 ? (desiredSize / usedEffectiveSize) : 1f;
|
|
|
|
var textSize = rawSize * scale;
|
|
|
|
var style = ImGui.GetStyle();
|
|
var frameHeight = desiredSize + style.FramePadding.Y * 2f;
|
|
var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
|
|
var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);
|
|
|
|
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
|
|
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
TargetDrawList = drawList,
|
|
ScreenOffset = drawPos,
|
|
Font = usedFont,
|
|
FontSize = desiredSize,
|
|
Color = 0xFFFFFFFF,
|
|
WrapWidth = float.MaxValue,
|
|
};
|
|
|
|
ImGui.SetCursorScreenPos(drawPos);
|
|
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
|
|
|
ImGui.SetCursorScreenPos(position);
|
|
ImGui.PushID(id ?? Interlocked.Increment(ref _seStringHitboxCounter).ToString());
|
|
|
|
try
|
|
{
|
|
ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
|
|
}
|
|
finally
|
|
{
|
|
ImGui.PopID();
|
|
}
|
|
|
|
return new Vector2(textSize.X, hitboxHeight);
|
|
}
|
|
|
|
public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null)
|
|
{
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var usedFont = font ?? UiBuilder.MonoFont;
|
|
var iconMacro = $"<icon({iconId})>";
|
|
|
|
var measureParams = new SeStringDrawParams
|
|
{
|
|
Font = usedFont,
|
|
FontSize = usedFont.FontSize,
|
|
Color = 0xFFFFFFFF,
|
|
WrapWidth = float.MaxValue
|
|
};
|
|
|
|
var measureResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, measureParams);
|
|
var iconSize = measureResult.Size;
|
|
if (iconSize.Y <= 0f)
|
|
{
|
|
iconSize.Y = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
|
|
}
|
|
|
|
var style = ImGui.GetStyle();
|
|
var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
|
|
var frameHeight = fontHeight + style.FramePadding.Y * 2f;
|
|
var hitboxHeight = MathF.Max(frameHeight, iconSize.Y);
|
|
var verticalOffset = MathF.Max((hitboxHeight - iconSize.Y) * 0.5f, 0f);
|
|
|
|
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
|
|
var drawParams = new SeStringDrawParams
|
|
{
|
|
Font = usedFont,
|
|
FontSize = usedFont.FontSize,
|
|
Color = 0xFFFFFFFF,
|
|
WrapWidth = float.MaxValue,
|
|
TargetDrawList = drawList,
|
|
ScreenOffset = drawPos
|
|
};
|
|
ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
|
|
|
|
ImGui.SetCursorScreenPos(position);
|
|
if (id is not null)
|
|
{
|
|
ImGui.PushID(id);
|
|
}
|
|
else
|
|
{
|
|
ImGui.PushID(Interlocked.Increment(ref _iconHitboxCounter));
|
|
}
|
|
|
|
try
|
|
{
|
|
ImGui.InvisibleButton("##iconHitbox", new Vector2(iconSize.X, hitboxHeight));
|
|
}
|
|
finally
|
|
{
|
|
ImGui.PopID();
|
|
}
|
|
|
|
return new Vector2(iconSize.X, hitboxHeight);
|
|
}
|
|
|
|
#region Internal Payloads
|
|
|
|
public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false);
|
|
|
|
private abstract class AbstractColorPayload : Payload
|
|
{
|
|
protected byte Red { get; init; }
|
|
protected byte Green { get; init; }
|
|
protected byte Blue { get; init; }
|
|
|
|
protected override byte[] EncodeImpl()
|
|
{
|
|
return new byte[] { 0x02, ChunkType, 0x05, 0xF6, Red, Green, Blue, 0x03 };
|
|
}
|
|
|
|
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }
|
|
|
|
public override PayloadType Type => PayloadType.Unknown;
|
|
protected abstract byte ChunkType { get; }
|
|
}
|
|
|
|
private abstract class AbstractColorEndPayload : Payload
|
|
{
|
|
protected override byte[] EncodeImpl()
|
|
{
|
|
return new byte[] { 0x02, ChunkType, 0x02, 0xEC, 0x03 };
|
|
}
|
|
|
|
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }
|
|
|
|
public override PayloadType Type => PayloadType.Unknown;
|
|
protected abstract byte ChunkType { get; }
|
|
}
|
|
|
|
private sealed class ColorPayload : AbstractColorPayload
|
|
{
|
|
protected override byte ChunkType => 0x13;
|
|
|
|
public ColorPayload(Vector3 color)
|
|
{
|
|
Red = Math.Max((byte)1, (byte)(color.X * 255f));
|
|
Green = Math.Max((byte)1, (byte)(color.Y * 255f));
|
|
Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
|
|
}
|
|
|
|
public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
|
|
}
|
|
|
|
private sealed class ColorEndPayload : AbstractColorEndPayload
|
|
{
|
|
protected override byte ChunkType => 0x13;
|
|
}
|
|
|
|
private sealed class GlowPayload : AbstractColorPayload
|
|
{
|
|
protected override byte ChunkType => 0x14;
|
|
|
|
public GlowPayload(Vector3 color)
|
|
{
|
|
Red = Math.Max((byte)1, (byte)(color.X * 255f));
|
|
Green = Math.Max((byte)1, (byte)(color.Y * 255f));
|
|
Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
|
|
}
|
|
|
|
public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
|
|
}
|
|
|
|
private sealed class GlowEndPayload : AbstractColorEndPayload
|
|
{
|
|
protected override byte ChunkType => 0x14;
|
|
}
|
|
|
|
#endregion
|
|
}
|