Fix UIColor handling per theme (#1995)

UIColor sheet has color sets per theme. Updated `UIColorWidget` to
reflect that, and added `SeStringDrawParams.ThemeIndex` to let users
choose which theme color set to use while drawing SeString from Dalamud.
This commit is contained in:
srkizer 2024-08-05 00:46:05 +09:00 committed by GitHub
parent eb2724f366
commit 694b42a378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 215 additions and 107 deletions

View file

@ -1,6 +1,5 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
@ -16,13 +15,9 @@ 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
/// <summary>Parsed <see cref="UIColor"/>, containing colors to use with <see cref="MacroCode.ColorType"/> and
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
private readonly uint[] edgeColorTypes;
private readonly uint[,] colorTypes;
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
@ -38,28 +33,30 @@ internal sealed class SeStringColorStackSet
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
/// <param name="uiColor">UIColor sheet.</param>
public SeStringColorStackSet(ExcelSheet<UIColor> uiColor)
public unsafe 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];
this.colorTypes = new uint[maxId + 1, 4];
foreach (var row in uiColor)
{
// Contains ABGR.
this.colorTypes[row.RowId] = row.UIForeground;
this.edgeColorTypes[row.RowId] = row.UIGlow;
this.colorTypes[row.RowId, 0] = row.UIForeground;
this.colorTypes[row.RowId, 1] = row.UIGlow;
this.colorTypes[row.RowId, 2] = row.Unknown0;
this.colorTypes[row.RowId, 3] = row.Unknown1;
}
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);
fixed (uint* p = this.colorTypes)
{
foreach (ref var r in new Span<uint>(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r);
}
}
}
@ -107,7 +104,8 @@ internal sealed class SeStringColorStackSet
internal void HandleShadowColorPayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload) =>
drawState.ShadowColor = ColorHelpers.ApplyOpacity(AdjustStack(this.shadowColorStack, payload), drawState.Opacity);
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>
@ -115,7 +113,9 @@ internal sealed class SeStringColorStackSet
internal void HandleColorTypePayload(
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload) =>
drawState.Color = ColorHelpers.ApplyOpacity(AdjustStack(this.colorStack, this.colorTypes, payload), drawState.Opacity);
drawState.Color = ColorHelpers.ApplyOpacity(
this.AdjustStackByType(this.colorStack, payload, drawState.ThemeIndex),
drawState.Opacity);
/// <summary>Handles a <see cref="MacroCode.EdgeColorType"/> payload.</summary>
/// <param name="drawState">Draw state.</param>
@ -124,19 +124,13 @@ internal sealed class SeStringColorStackSet
scoped ref SeStringDrawState drawState,
ReadOnlySePayloadSpan payload)
{
var newColor = AdjustStack(this.edgeColorStack, this.edgeColorTypes, payload);
var newColor = this.AdjustStackByType(this.edgeColorStack, payload, drawState.ThemeIndex);
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))
@ -154,7 +148,10 @@ internal sealed class SeStringColorStackSet
if (expr.TryGetUInt(out var bgra))
{
rgbaStack.Add(SwapRedBlue(bgra) | 0xFF000000u);
// NOTE: if it reads a `0`, then it seems to be doing something else.
// See case 0x12 from `Component::GUI::AtkFontAnalyzerBase.vf4`.
// Fix when someone figures what's this about.
rgbaStack.Add(ColorHelpers.SwapRedBlue(bgra) | 0xFF000000u);
return rgbaStack[^1];
}
@ -166,7 +163,7 @@ internal sealed class SeStringColorStackSet
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);
rgbaStack.Add(ColorHelpers.SwapRedBlue((uint)gp.IntValue) | 0xFF000000u);
return rgbaStack[^1];
}
@ -175,13 +172,14 @@ internal sealed class SeStringColorStackSet
return rgbaStack[^1];
}
private static uint AdjustStack(List<uint> rgbaStack, uint[] colorTypes, ReadOnlySePayloadSpan payload)
private uint AdjustStackByType(List<uint> rgbaStack, ReadOnlySePayloadSpan payload, int themeIndex)
{
if (!payload.TryGetExpression(out var expr))
return rgbaStack[^1];
if (!expr.TryGetUInt(out var colorTypeIndex))
return rgbaStack[^1];
// Component::GUI::AtkFontAnalyzerBase.vf4: passing 0 will pop the color off the stack.
if (colorTypeIndex == 0)
{
// First item in the stack is the color we started to draw with.
@ -191,8 +189,12 @@ internal sealed class SeStringColorStackSet
}
// Opacity component is ignored.
rgbaStack.Add((colorTypeIndex < colorTypes.Length ? colorTypes[colorTypeIndex] : 0u) | 0xFF000000u);
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) &&
colorTypeIndex < this.colorTypes.GetLength(0)
? this.colorTypes[colorTypeIndex, themeIndex]
: 0u;
rgbaStack.Add(color | 0xFF000000u);
return rgbaStack[^1];
}
}

View file

@ -547,9 +547,8 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
Rune lastRune,
int link)
{
var gfdTextureSrv =
(nint)UIModule.Instance()->GetRaptureAtkModule()->AtkModule.AtkFontManager.Gfd->Texture->
D3D11ShaderResourceView;
// This might temporarily return 0 while logging in.
var gfdTextureSrv = GetGfdTextureSrv();
var x = 0f;
var width = 0f;
foreach (var c in UtfEnumerator.From(span, UtfEnumeratorFlags.Utf8SeString))
@ -569,13 +568,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
{
var size = gfdEntry.CalculateScaledSize(state.FontSize, out var useHq);
state.SetCurrentChannel(SeStringDrawChannel.Foreground);
state.Draw(
gfdTextureSrv,
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
ColorHelpers.ApplyOpacity(uint.MaxValue, state.Opacity));
if (gfdTextureSrv != 0)
{
state.Draw(
gfdTextureSrv,
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1,
ColorHelpers.ApplyOpacity(uint.MaxValue, state.Opacity));
}
if (link != -1)
state.DrawLinkUnderline(offset + new Vector2(x, 0), size.X);
@ -602,6 +605,29 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale));
x += dist + advanceWidth;
}
return;
static nint GetGfdTextureSrv()
{
var uim = UIModule.Instance();
if (uim is null)
return 0;
var ram = uim->GetRaptureAtkModule();
if (ram is null)
return 0;
var gfd = ram->AtkModule.AtkFontManager.Gfd;
if (gfd is null)
return 0;
var tex = gfd->Texture;
if (tex is null)
return 0;
return (nint)tex->D3D11ShaderResourceView;
}
}
/// <summary>Determines a bitmap icon to display for the given SeString payload.</summary>

View file

@ -56,6 +56,13 @@ public record struct SeStringDrawParams
/// 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 theme that will decide the colors to use for <see cref="MacroCode.ColorType"/>
/// and <see cref="MacroCode.EdgeColorType"/>.</summary>
/// <value><c>0</c> to use colors for Dark theme, <c>1</c> to use colors for Light theme, <c>2</c> to use colors
/// for Classic FF theme, <c>3</c> to use colors for Clear Blue theme, or <c>null</c> to use the theme set from the
/// game configuration.</value>
public int? ThemeIndex { 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; }

View file

@ -6,6 +6,8 @@ using System.Text;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Lumina.Text.Payloads;
@ -55,6 +57,7 @@ public unsafe ref struct SeStringDrawState
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ForceEdgeColor = ssdp.ForceEdgeColor;
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
this.Bold = ssdp.Bold;
this.Italic = ssdp.Italic;
this.Edge = ssdp.Edge;
@ -100,6 +103,9 @@ public unsafe ref struct SeStringDrawState
/// <inheritdoc cref="SeStringDrawParams.EdgeStrength"/>
public float EdgeOpacity { get; }
/// <inheritdoc cref="SeStringDrawParams.ThemeIndex"/>
public int ThemeIndex { get; }
/// <inheritdoc cref="SeStringDrawParams.Color"/>
public uint Color { get; set; }