Merge remote-tracking branch 'origin/master' into api14-rollup
Some checks failed
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled

This commit is contained in:
github-actions[bot] 2025-12-04 22:03:13 +00:00
commit 6f8e33a39c
33 changed files with 1057 additions and 496 deletions

View file

@ -487,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
#pragma warning disable SA1600
#pragma warning disable SA1516
// XLCore/XoM compatibility until they move it out
public string? DalamudBetaKey { get; set; } = null;
public string? DalamudBetaKind { get; set; }
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary>
/// Load a configuration from the provided path.
/// </summary>

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.11</DalamudVersion>
<DalamudVersion>13.0.0.12</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>

View file

@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
/// <inheritdoc/>
public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default;
public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
/// <inheritdoc/>
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;

View file

@ -60,8 +60,8 @@ internal record struct NounParams()
/// </summary>
public readonly int ColumnOffset => this.SheetName switch
{
// See "E8 ?? ?? ?? ?? 44 8B 6B 08"
nameof(LSheets.BeastTribe) => 10,
// See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
nameof(LSheets.BeastTribe) => 11,
nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1,

View file

@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
exStyle =
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
if (flags.HasFlag(ImGuiViewportFlags.TopMost))
if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST;
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
}
}

View file

@ -8,6 +8,7 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Memory;
using Dalamud.Utility;
using Serilog;
@ -34,11 +35,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate;
private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr;
private ViewportHandler viewportHandler;
private int mouseButtonsDown;
private bool mouseTracked;
private long lastTime;
private nint iniPathPtr;
@ -64,7 +66,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports |
ImGuiBackendFlags.PlatformHasViewports;
ImGuiBackendFlags.PlatformHasViewports |
ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@ -74,8 +77,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this);
this.imguiMouseIsDown = new bool[5];
this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@ -95,8 +96,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
/// <inheritdoc/>
public bool UpdateCursor { get; set; } = true;
@ -155,6 +154,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight)
{
var io = ImGui.GetIO();
var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight;
@ -168,9 +168,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
this.UpdateMousePos();
this.UpdateMouseData(focusedWindow);
this.ProcessKeyEventsWorkarounds();
this.ProcessKeyEventsWorkarounds(focusedWindow);
// TODO: need to figure out some way to unify all this
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
@ -224,6 +224,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
switch (msg)
{
case WM.WM_MOUSEMOVE:
{
if (!this.mouseTracked)
{
var tme = new TRACKMOUSEEVENT
{
cbSize = (uint)sizeof(TRACKMOUSEEVENT),
dwFlags = TME.TME_LEAVE,
hwndTrack = hWndCurrent,
};
this.mouseTracked = TrackMouseEvent(&tme);
}
var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(hWndCurrent, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
break;
}
case WM.WM_MOUSELEAVE:
{
this.mouseTracked = false;
var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
ClientToScreen(hWndCurrent, &mouseScreenPos);
if (this.ViewportFromPoint(mouseScreenPos).IsNull)
{
var fltMax = ImGuiNative.GETFLTMAX();
io.AddMousePosEvent(-fltMax, -fltMax);
}
break;
}
case WM.WM_LBUTTONDOWN:
case WM.WM_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN:
@ -236,11 +270,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
var button = GetButton(msg, wParam);
if (io.WantCaptureMouse)
{
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
SetCapture(hWndCurrent);
io.MouseDown[button] = true;
this.imguiMouseIsDown[button] = true;
this.mouseButtonsDown |= 1 << button;
io.AddMouseButtonEvent(button, true);
return default(LRESULT);
}
@ -256,13 +289,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONUP:
{
var button = GetButton(msg, wParam);
if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
if (io.WantCaptureMouse)
{
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
this.mouseButtonsDown &= ~(1 << button);
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
ReleaseCapture();
io.MouseDown[button] = false;
this.imguiMouseIsDown[button] = false;
io.AddMouseButtonEvent(button, false);
return default(LRESULT);
}
@ -272,7 +304,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL:
if (io.WantCaptureMouse)
{
io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
return default(LRESULT);
}
@ -280,7 +312,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL:
if (io.WantCaptureMouse)
{
io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
return default(LRESULT);
}
@ -374,68 +406,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
ReleaseCapture();
case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
io.AddFocusEvent(true);
break;
ImGui.GetIO().WantCaptureMouse = false;
ImGui.ClearWindowFocus();
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
io.AddFocusEvent(false);
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
// ReleaseCapture();
//
// ImGui.GetIO().WantCaptureMouse = false;
// ImGui.ClearWindowFocus();
break;
}
return null;
}
private void UpdateMousePos()
private void UpdateMouseData(HWND focusedWindow)
{
var io = ImGui.GetIO();
var pt = default(POINT);
// Depending on if Viewports are enabled, we have to change how we process
// the cursor position. If viewports are enabled, we pass the absolute cursor
// position to ImGui. Otherwise, we use the old method of passing client-local
// mouse position to ImGui.
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
var mouseScreenPos = default(POINT);
var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
var isAppFocused =
focusedWindow != default
&& (focusedWindow == this.hWnd
|| IsChild(focusedWindow, this.hWnd)
|| !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
if (isAppFocused)
{
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
// When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
if (io.WantSetMousePos)
{
SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y);
var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
}
}
if (GetCursorPos(&pt))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{
// Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
// (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
// Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
// (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
var mousePos = mouseScreenPos;
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
ClientToScreen(focusedWindow, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
}
// (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
// If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
// - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
// Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
// for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
// by the backend, and use its flawed heuristic to guess the viewport behind.
// - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
if (hasMouseScreenPos)
{
var viewport = this.ViewportFromPoint(mouseScreenPos);
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
}
else
{
if (io.WantSetMousePos)
{
pt.x = (int)io.MousePos.X;
pt.y = (int)io.MousePos.Y;
ClientToScreen(this.hWnd, &pt);
SetCursorPos(pt.x, pt.y);
}
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
io.AddMouseViewportEvent(0);
}
}
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
{
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
}
private bool UpdateMouseCursor()
{
var io = ImGui.GetIO();
@ -451,7 +501,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return true;
}
private void ProcessKeyEventsWorkarounds()
private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
{
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@ -480,7 +530,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
// See: https://github.com/goatcorp/ImGuiScene/pull/13
// > GetForegroundWindow from winuser.h is a surprisingly expensive function.
var isForeground = GetForegroundWindow() == this.hWnd;
var isForeground = focusedWindow == this.hWnd;
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
{
// Skip raising modifier keys if the game is focused.
@ -646,14 +696,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return;
var pio = ImGui.GetPlatformIO();
if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
{
// We allocated the platform monitor data in OnUpdateMonitors ourselves,
// so we have to free it ourselves to ImGui doesn't try to, or else it will crash
Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
ImGui.GetPlatformIO().Handle->Monitors = default;
}
ImGui.GetPlatformIO().Handle->Monitors.Free();
fixed (char* windowClassNamePtr = WindowClassName)
{
@ -693,59 +736,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// Here we use a manual ImVector overload, free the existing monitor data,
// and allocate our own, as we are responsible for telling ImGui about monitors
var pio = ImGui.GetPlatformIO();
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS);
var data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
if (pio.Handle->Monitors.Data != null)
Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
pio.Handle->Monitors.Resize(0);
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
// Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
// int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
// nint data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
// platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
var monitorIndex = -1;
var enumfn = new MonitorEnumProcDelegate(
(hMonitor, _, _, _) =>
{
monitorIndex++;
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
imMonitor.MainPos = monitorLt;
imMonitor.MainSize = monitorRb - monitorLt;
imMonitor.WorkPos = workLt;
imMonitor.WorkSize = workRb - workLt;
imMonitor.DpiScale = 1f;
return true;
});
EnumDisplayMonitors(
default,
null,
(delegate* unmanaged<HMONITOR, HDC, RECT*, LPARAM, BOOL>)Marshal.GetFunctionPointerForDelegate(enumfn),
default);
EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
Log.Information("Monitors set up!");
for (var i = 0; i < numMonitors; i++)
foreach (ref var monitor in pio.Handle->Monitors)
{
var monitor = pio.Handle->Monitors[i];
Log.Information(
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}",
i,
"Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
monitor.MainPos,
monitor.MainSize,
monitor.WorkPos,
monitor.WorkSize);
}
return;
[UnmanagedCallersOnly]
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
{
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
var imMonitor = new ImGuiPlatformMonitor
{
MainPos = monitorLt,
MainSize = monitorRb - monitorLt,
WorkPos = workLt,
WorkSize = workRb - workLt,
DpiScale = 1f,
};
if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
else
ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
return true;
}
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null);
}
if (data->Hwnd == 0)
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
data->HwndOwned = true;
viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;

View file

@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Color stacks to use while evaluating a SeString.</summary>
internal sealed class SeStringColorStackSet
{
/// <summary>Parsed <see cref="UIColor"/>, containing colors to use with <see cref="MacroCode.ColorType"/> and
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
private readonly uint[,] colorTypes;
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> colorStack = [];
@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
foreach (var row in uiColor)
maxId = (int)Math.Max(row.RowId, maxId);
this.colorTypes = new uint[maxId + 1, 4];
this.ColorTypes = new uint[maxId + 1, 4];
foreach (var row in uiColor)
{
// Contains ABGR.
this.colorTypes[row.RowId, 0] = row.Dark;
this.colorTypes[row.RowId, 1] = row.Light;
this.colorTypes[row.RowId, 2] = row.ClassicFF;
this.colorTypes[row.RowId, 3] = row.ClearBlue;
this.ColorTypes[row.RowId, 0] = row.Dark;
this.ColorTypes[row.RowId, 1] = row.Light;
this.ColorTypes[row.RowId, 2] = row.ClassicFF;
this.ColorTypes[row.RowId, 3] = row.ClearBlue;
}
if (BitConverter.IsLittleEndian)
{
// ImGui wants RGBA in LE.
fixed (uint* p = this.colorTypes)
fixed (uint* p = this.ColorTypes)
{
foreach (ref var r in new Span<uint>(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1)))
foreach (ref var r in new Span<uint>(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r);
}
}
}
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
/// <param name="colorTypes">Color types.</param>
public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
/// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary>
public bool HasAdditionalEdgeColor { get; private set; }
/// <summary>Gets the parsed <see cref="UIColor"/> containing colors to use with <see cref="MacroCode.ColorType"/>
/// and <see cref="MacroCode.EdgeColorType"/>.</summary>
public uint[,] ColorTypes { get; }
/// <summary>Resets the colors in the stack.</summary>
/// <param name="drawState">Draw state.</param>
internal void Initialize(scoped ref SeStringDrawState drawState)
@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
}
// Opacity component is ignored.
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) &&
colorTypeIndex < this.colorTypes.GetLength(0)
? this.colorTypes[colorTypeIndex, themeIndex]
var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
colorTypeIndex < this.ColorTypes.GetLength(0)
? this.ColorTypes[colorTypeIndex, themeIndex]
: 0u;
rgbaStack.Add(color | 0xFF000000u);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Draws SeString.</summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class SeStringRenderer : IInternalDisposableService
internal class SeStringRenderer : IServiceType
{
private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118;
@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Parsed text fragments from a SeString.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly List<TextFragment> fragments = [];
private readonly List<TextFragment> fragmentsMainThread = [];
/// <summary>Color stacks to use while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks>
private readonly SeStringColorStackSet colorStackSet;
/// <summary>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary>
private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
private readonly SeStringColorStackSet colorStackSetMainThread;
[ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{
this.colorStackSet = new(dm.Excel.GetSheet<UIColor>());
this.colorStackSetMainThread = new(dm.Excel.GetSheet<UIColor>());
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
}
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
~SeStringRenderer() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
/// <summary>Compiles and caches a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param>
@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
text.ReplaceLineEndings("<br>"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
/// <param name="sss">SeString to render.</param>
/// <param name="drawParams">Parameters for drawing.</param>
/// <returns>A new self-contained draw data.</returns>
public unsafe BufferBackedImDrawData CreateDrawData(
ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default)
{
if (drawParams.TargetDrawList is not null)
{
throw new ArgumentException(
$"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.",
nameof(drawParams));
}
var dd = BufferBackedImDrawData.Create();
try
{
var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size;
var offset = drawParams.ScreenOffset ?? Vector2.Zero;
foreach (var vtx in new Span<ImDrawVert>(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size))
offset = Vector2.Min(offset, vtx.Pos);
dd.Data.DisplayPos = offset;
dd.Data.DisplaySize = size - offset;
dd.Data.Valid = 1;
dd.UpdateDrawDataStatistics();
return dd;
}
catch
{
dd.Dispose();
throw;
}
}
/// <summary>Compiles 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 newline payloads.</param>
@ -113,28 +143,42 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <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>
public SeStringDrawResult Draw(
public unsafe SeStringDrawResult Draw(
ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default,
ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
{
// Drawing is only valid if done from the main thread anyway, especially with interactivity.
ThreadSafety.AssertMainThread();
// Interactivity is supported only from the main thread.
if (!imGuiId.IsEmpty())
ThreadSafety.AssertMainThread();
if (drawParams.TargetDrawList is not null && imGuiId)
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
// This also does argument validation for drawParams. Do it here.
var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
using var cleanup = new DisposeSafety.ScopedFinalizer();
// Reset and initialize the state.
this.fragments.Clear();
this.colorStackSet.Initialize(ref state);
ImFont* font = null;
if (drawParams.Font.HasValue)
font = drawParams.Font.Value;
if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
font = ImGui.GetFont();
if (font is null)
throw new ArgumentException("Specified font is empty.");
// This also does argument validation for drawParams. Do it here.
// `using var` makes a struct read-only, but we do want to modify it.
var stateStorage = new SeStringDrawState(
sss,
drawParams,
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
ThreadSafety.IsMainThread ? this.fragmentsMainThread : [],
font);
ref var state = ref Unsafe.AsRef(in stateStorage);
// Analyze the provided SeString and break it up to text fragments.
this.CreateTextFragments(ref state);
var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments);
var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
// Calculate size.
var size = Vector2.Zero;
@ -147,24 +191,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
state.SplitDrawList();
// Handle cases where ImGui.AlignTextToFramePadding has been called.
var context = ImGui.GetCurrentContext();
var currLineTextBaseOffset = 0f;
if (!context.IsNull)
{
var currentWindow = context.CurrentWindow;
if (!currentWindow.IsNull)
{
currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset;
}
}
var itemSize = size;
if (currLineTextBaseOffset != 0f)
if (drawParams.TargetDrawList is null)
{
itemSize.Y += 2 * currLineTextBaseOffset;
foreach (ref var f in fragmentSpan)
f.Offset += new Vector2(0, currLineTextBaseOffset);
// Handle cases where ImGui.AlignTextToFramePadding has been called.
var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
if (currLineTextBaseOffset != 0f)
{
itemSize.Y += 2 * currLineTextBaseOffset;
foreach (ref var f in fragmentSpan)
f.Offset += new Vector2(0, currLineTextBaseOffset);
}
}
// Draw all text fragments.
@ -280,15 +317,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0;
}
private void ReleaseUnmanagedResources()
{
if (this.splitter is not null)
{
this.splitter->Destroy();
this.splitter = null;
}
}
/// <summary>Creates text fragment, taking line and word breaking into account.</summary>
/// <param name="state">Draw state.</param>
private void CreateTextFragments(ref SeStringDrawState state)
@ -391,7 +419,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
// Test if the fragment does not fit into the current line and the current line is not empty.
if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows)
{
// Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity.
@ -401,7 +429,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
xy.X = 0;
xy.Y += state.LineHeight;
w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
fragment.Offset = xy;
// Now that the fragment is given its own line, test if it overflows again.
@ -419,16 +447,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
}
}
else if (this.fragments.Count > 0 && xy.X != 0)
else if (state.Fragments.Count > 0 && xy.X != 0)
{
// New fragment fits into the current line, and it has a previous fragment in the same line.
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
// trailing soft hyphens are not considered.
if (this.fragments[^1].EndsWithSoftHyphen)
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
if (state.Fragments[^1].EndsWithSoftHyphen)
xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
// Adjust this fragment's offset from kerning distance.
xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune);
xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune);
fragment.Offset = xy;
}
@ -439,7 +467,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth;
prev = fragment.To;
this.fragments.Add(fragment);
state.Fragments.Add(fragment);
if (fragment.BreakAfter)
{
@ -491,7 +519,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (gfdTextureSrv != 0)
{
state.Draw(
new ImTextureID(gfdTextureSrv),
new(gfdTextureSrv),
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
@ -528,7 +556,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return;
static nint GetGfdTextureSrv()
static unsafe nint GetGfdTextureSrv()
{
var uim = UIModule.Instance();
if (uim is null)
@ -553,7 +581,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Determines a bitmap icon to display for the given SeString payload.</summary>
/// <param name="sss">Byte span that should include a SeString payload.</param>
/// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns>
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
private unsafe 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)
@ -710,38 +738,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
firstDisplayRune ?? default,
lastNonSoftHyphenRune);
}
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
private record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}
}

View file

@ -0,0 +1,39 @@
using System.Numerics;
using System.Text;
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
internal record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
/// <summary>Gets a value indicating whether the fragment ends with a visible soft hyphen.</summary>
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}

View file

@ -12,7 +12,10 @@ 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>
/// If this value is set, <see cref="ImGui.Dummy"/> will not be called, and ImGui ID will be ignored.
/// You <b>must</b> specify a valid draw list and a valid font via <see cref="Font"/> if you set this value,
/// since the renderer will not be able to retrieve them from ImGui context.
/// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; }

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -6,6 +7,8 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@ -19,46 +22,75 @@ public unsafe ref struct SeStringDrawState
private static readonly int ChannelCount = Enum.GetValues<SeStringDrawChannel>().Length;
private readonly ImDrawList* drawList;
private readonly SeStringColorStackSet colorStackSet;
private readonly ImDrawListSplitter* splitter;
private ImDrawListSplitter splitter;
/// <summary>Initializes a new instance of the <see cref="SeStringDrawState"/> struct.</summary>
/// <param name="span">Raw SeString byte span.</param>
/// <param name="ssdp">Instance of <see cref="SeStringDrawParams"/> to initialize from.</param>
/// <param name="colorStackSet">Instance of <see cref="SeStringColorStackSet"/> to use.</param>
/// <param name="splitter">Instance of ImGui Splitter to use.</param>
/// <param name="fragments">Fragments.</param>
/// <param name="font">Font to use.</param>
internal SeStringDrawState(
ReadOnlySpan<byte> span,
scoped in SeStringDrawParams ssdp,
SeStringColorStackSet colorStackSet,
ImDrawListSplitter* splitter)
List<TextFragment> fragments,
ImFont* font)
{
this.colorStackSet = colorStackSet;
this.splitter = splitter;
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.Span = span;
this.ColorStackSet = colorStackSet;
this.Fragments = fragments;
this.Font = font;
if (ssdp.TargetDrawList is null)
{
if (!ThreadSafety.IsMainThread)
{
throw new ArgumentException(
$"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread.");
}
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
}
else
{
this.drawList = ssdp.TargetDrawList.Value;
this.ScreenOffset = Vector2.Zero;
this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
$"{nameof(ssdp.FontSize)} must be set to render outside the main thread.");
this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
this.Color = ssdp.Color ?? uint.MaxValue;
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread.
this.ThemeIndex = ssdp.ThemeIndex ?? 0;
}
this.splitter = default;
this.GetEntity = ssdp.GetEntity;
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
this.Font = ssdp.EffectiveFont;
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.FontSizeScale = this.FontSize / this.Font->FontSize;
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
this.Opacity = ssdp.EffectiveOpacity;
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ForceEdgeColor = ssdp.ForceEdgeColor;
this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
this.Bold = ssdp.Bold;
this.Italic = ssdp.Italic;
this.Edge = ssdp.Edge;
this.Shadow = ssdp.Shadow;
this.ColorStackSet.Initialize(ref this);
fragments.Clear();
}
/// <inheritdoc cref="SeStringDrawParams.TargetDrawList"/>
@ -135,7 +167,7 @@ public unsafe ref struct SeStringDrawState
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawEdge =>
(this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
(this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
@ -143,11 +175,17 @@ public unsafe ref struct SeStringDrawState
/// <summary>Gets a value indicating whether the edge should be drawn.</summary>
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
/// <summary>Gets the color stacks.</summary>
internal SeStringColorStackSet ColorStackSet { get; }
/// <summary>Gets the text fragments.</summary>
internal List<TextFragment> Fragments { get; }
/// <summary>Sets the current channel in the ImGui draw list splitter.</summary>
/// <param name="channelIndex">Channel to switch to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex);
public void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex);
/// <summary>Draws a single texture.</summary>
/// <param name="igTextureId">ImGui texture ID to draw from.</param>
@ -216,7 +254,7 @@ public unsafe ref struct SeStringDrawState
/// <summary>Draws a single glyph using current styling configurations.</summary>
/// <param name="g">Glyph to draw.</param>
/// <param name="offset">Offset of the glyph in pixels w.r.t. <see cref="ScreenOffset"/>.</param>
internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
{
var texId = this.Font->ContainerAtlas->Textures.Ref<ImFontAtlasTexture>(g.TextureIndex).TexID;
var xy0 = new Vector2(
@ -268,7 +306,7 @@ public unsafe ref struct SeStringDrawState
/// <param name="offset">Offset of the glyph in pixels w.r.t.
/// <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="advanceWidth">Advance width of the glyph.</param>
internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
internal void DrawLinkUnderline(Vector2 offset, float advanceWidth)
{
if (this.LinkUnderlineThickness < 1f)
return;
@ -350,15 +388,15 @@ public unsafe ref struct SeStringDrawState
switch (payload.MacroCode)
{
case MacroCode.Color:
this.colorStackSet.HandleColorPayload(ref this, payload);
this.ColorStackSet.HandleColorPayload(ref this, payload);
return true;
case MacroCode.EdgeColor:
this.colorStackSet.HandleEdgeColorPayload(ref this, payload);
this.ColorStackSet.HandleEdgeColorPayload(ref this, payload);
return true;
case MacroCode.ShadowColor:
this.colorStackSet.HandleShadowColorPayload(ref this, payload);
this.ColorStackSet.HandleShadowColorPayload(ref this, payload);
return true;
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
@ -379,11 +417,11 @@ public unsafe ref struct SeStringDrawState
return true;
case MacroCode.ColorType:
this.colorStackSet.HandleColorTypePayload(ref this, payload);
this.ColorStackSet.HandleColorTypePayload(ref this, payload);
return true;
case MacroCode.EdgeColorType:
this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload);
this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload);
return true;
default:
@ -393,10 +431,9 @@ public unsafe ref struct SeStringDrawState
/// <summary>Splits the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void SplitDrawList() =>
this.splitter->Split(this.drawList, ChannelCount);
internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
/// <summary>Merges the draw list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList);
internal void MergeDrawList() => this.splitter.Merge(this.drawList);
}

View file

@ -533,6 +533,13 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Restart();
}
/// <inheritdoc cref="DataWindow.GetWidget{T}"/>
public T GetDataWindowWidget<T>() where T : IDataWindowWidget => this.dataWindow.GetWidget<T>();
/// <summary>Sets the data window current widget.</summary>
/// <param name="widget">Widget to set current.</param>
public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
private void OnDraw()
{
this.FrameCount++;

View file

@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept;
private bool selectionCollapsed;
private IDataWindowWidget currentWidget;
private bool isLoaded;
/// <summary>
@ -82,9 +82,12 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
this.currentWidget = this.orderedModules.First();
this.CurrentWidget = this.orderedModules.First();
}
/// <summary>Gets or sets the current widget.</summary>
public IDataWindowWidget CurrentWidget { get; set; }
/// <inheritdoc/>
public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose();
@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable
{
}
/// <summary>Gets the data window widget of the specified type.</summary>
/// <typeparam name="T">Type of the data window widget to find.</typeparam>
/// <returns>Found widget.</returns>
public T GetWidget<T>() where T : IDataWindowWidget
{
foreach (var m in this.modules)
{
if (m is T w)
return w;
}
throw new ArgumentException($"No widget of type {typeof(T).FullName} found.");
}
/// <summary>
/// Set the DataKind dropdown menu.
/// </summary>
@ -110,7 +127,7 @@ internal class DataWindow : Window, IDisposable
if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule)
{
this.currentWidget = targetModule;
this.CurrentWidget = targetModule;
}
else
{
@ -153,9 +170,9 @@ internal class DataWindow : Window, IDisposable
{
foreach (var widget in this.orderedModules)
{
if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget))
if (ImGui.Selectable(widget.DisplayName, this.CurrentWidget == widget))
{
this.currentWidget = widget;
this.CurrentWidget = widget;
}
}
@ -206,9 +223,9 @@ internal class DataWindow : Window, IDisposable
try
{
if (this.currentWidget is { Ready: true })
if (this.CurrentWidget is { Ready: true })
{
this.currentWidget.Draw();
this.CurrentWidget.Draw();
}
else
{

View file

@ -1,9 +1,15 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Lumina.Text.ReadOnly;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -87,12 +93,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10f);
for (var i = 0; i < this.icons?.Count; i++)
{
if (this.icons[i] == FontAwesomeIcon.None)
continue;
ImGui.AlignTextToFramePadding();
ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}");
ImGuiHelpers.ScaledRelativeSameLine(50f);
ImGui.Text($"{this.iconNames?[i]}");
ImGuiHelpers.ScaledRelativeSameLine(280f);
ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
ImGui.Text(this.icons[i].ToIconString());
ImGuiHelpers.ScaledRelativeSameLine(320f);
if (this.useFixedWidth
? ImGui.Button($"{(char)this.icons[i]}##FontAwesomeIconButton{i}")
: ImGuiComponents.IconButton($"##FontAwesomeIconButton{i}", this.icons[i]))
{
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.DisplayName,
this.icons[i].ToString(),
Task.FromResult(
Service<TextureManager>.Get().CreateTextureFromSeString(
ReadOnlySeString.FromText(this.icons[i].ToIconString()),
new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() })));
}
ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f);
}

View file

@ -1,5 +1,6 @@
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Data;
@ -9,11 +10,13 @@ using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.Parse;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@ -56,11 +59,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
/// <inheritdoc/>
public void Draw()
{
var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text));
var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ??= ImGui.GetColorU32(ImGuiCol.Text));
if (ImGui.ColorEdit4("Color", ref t2))
this.style.Color = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ??= 0xFF000000u);
if (ImGui.ColorEdit4("Edge Color", ref t2))
this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2);
@ -69,27 +72,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Checkbox("Forced"u8, ref t))
this.style.ForceEdgeColor = t;
t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u);
if (ImGui.ColorEdit4("Shadow Color", ref t2))
t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ??= 0xFF000000u);
if (ImGui.ColorEdit4("Shadow Color"u8, ref t2))
this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered));
if (ImGui.ColorEdit4("Link Hover Color", ref t2))
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonHovered));
if (ImGui.ColorEdit4("Link Hover Color"u8, ref t2))
this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2);
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive));
if (ImGui.ColorEdit4("Link Active Color", ref t2))
t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonActive));
if (ImGui.ColorEdit4("Link Active Color"u8, ref t2))
this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2);
var t3 = this.style.LineHeight ?? 1f;
var t3 = this.style.LineHeight ??= 1f;
if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f"))
this.style.LineHeight = t3;
t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha;
t3 = this.style.Opacity ??= 1f;
if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.Opacity = t3;
t3 = this.style.EdgeStrength ?? 0.25f;
t3 = this.style.EdgeStrength ??= 0.25f;
if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.EdgeStrength = t3;
@ -240,6 +243,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
Service<SeStringRenderer>.Get().CompileAndCache(this.testString).Data.Span));
}
ImGui.SameLine();
if (ImGui.Button("Copy as Image"))
{
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.DisplayName,
$"From {nameof(SeStringRendererTestWidget)}",
Task.FromResult(
Service<TextureManager>.Get().CreateTextureFromSeString(
ReadOnlySeString.FromMacroString(
this.testString,
new(ExceptionMode: MacroStringParseExceptionMode.EmbedError)),
this.style with
{
Font = ImGui.GetFont(),
FontSize = ImGui.GetFontSize(),
WrapWidth = ImGui.GetContentRegionAvail().X,
ThemeIndex = AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType,
})));
}
ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped(
"Optional features implemented for the following test input:<br>" +

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@ -306,12 +306,12 @@ internal class TexWidget : IDataWindowWidget
pres->Release();
ImGui.Text($"RC: Resource({rcres})/View({rcsrv})");
ImGui.Text(source.ToString());
ImGui.Text($"{source.Width} x {source.Height} | {source}");
}
else
{
ImGui.Text("RC: -"u8);
ImGui.Text(" "u8);
ImGui.Text("RC: -");
ImGui.Text(string.Empty);
}
}
@ -342,6 +342,10 @@ internal class TexWidget : IDataWindowWidget
runLater?.Invoke();
}
/// <summary>Adds a texture wrap for debug display purposes.</summary>
/// <param name="textureTask">Task returning a texture.</param>
public void AddTexture(Task<IDalamudTextureWrap> textureTask) => this.addedTextures.Add(new(Api10: textureTask));
private unsafe void DrawBlame(List<TextureManager.IBlameableDalamudTextureWrap> allBlames)
{
var im = Service<InterfaceManager>.Get();

View file

@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"),
// ColumnOffset tests
new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"),
new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"),
new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"),
new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"),
new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"),
new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"),
new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"),
new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"),
new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"),
new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"),
new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"),
new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"),
new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"),
new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"),
];
private enum GermanCases

View file

@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
: base(
"TitleScreenMenuOverlay",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus |
ImGuiWindowFlags.NoDocking)
{
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
@ -33,10 +34,22 @@ public interface IFontHandle : IDisposable
/// </summary>
/// <remarks>
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>.
/// Alternatively, use <see cref="WaitAsync()"/> to wait for this property to become <c>true</c>.
/// </remarks>
bool Available { get; }
/// <summary>
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for use in any thread.<br />
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
/// </summary>
/// <param name="errorMessage">The error message, if any.</param>
/// <returns>
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
/// </returns>
ILockedImFont? TryLock(out string? errorMessage);
/// <summary>
/// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for use in any thread.<br />
@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable
/// </summary>
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
Task<IFontHandle> WaitAsync();
/// <summary>
/// Waits for <see cref="Available"/> to become <c>true</c>.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
Task<IFontHandle> WaitAsync(CancellationToken cancellationToken);
}

View file

@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle
}
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync()
public Task<IFontHandle> WaitAsync() => this.WaitAsync(CancellationToken.None);
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken)
{
if (this.Available)
return Task.FromResult<IFontHandle>(this);
var tcs = new TaskCompletionSource<IFontHandle>(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => tcs.TrySetCanceled());
this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed;
if (this.Available)

View file

@ -0,0 +1,35 @@
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Utility;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
[ServiceManager.ServiceDependency]
private readonly SeStringRenderer seStringRenderer = Service<SeStringRenderer>.Get();
/// <inheritdoc/>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null)
{
ThreadSafety.AssertMainThread();
using var dd = this.seStringRenderer.CreateDrawData(text, drawParams);
var texture = this.CreateDrawListTexture(debugName ?? nameof(this.CreateTextureFromSeString));
try
{
texture.Size = dd.Data.DisplaySize;
texture.Draw(dd.DataPtr);
return texture;
}
catch
{
texture.Dispose();
throw;
}
}
}

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
@ -248,7 +249,7 @@ internal sealed partial class TextureManager
usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
else
usage = D3D11_USAGE.D3D11_USAGE_DEFAULT;
using var texture = this.device.CreateTexture2D(
new()
{

View file

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.IoC;
@ -283,6 +284,18 @@ internal sealed class TextureManagerPluginScoped
return textureWrap;
}
/// <inheritdoc/>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null)
{
var manager = this.ManagerOrThrow;
var textureWrap = manager.CreateTextureFromSeString(text, drawParams, debugName);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
@ -691,13 +692,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true,
string? debugName = null) =>
this.scopedFinalizer.Add(Service<FontAtlasFactory>
.Get()
.CreateFontAtlas(
this.namespaceName + ":" + (debugName ?? "custom"),
autoRebuildMode,
isGlobalScaled,
this.plugin));
this.scopedFinalizer.Add(
Service<FontAtlasFactory>
.Get()
.CreateFontAtlas(
this.namespaceName + ":" + (debugName ?? "custom"),
autoRebuildMode,
isGlobalScaled,
this.plugin));
/// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code.
@ -868,6 +870,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
// Note: do not dispose w; we do not own it
}
public ILockedImFont? TryLock(out string? errorMessage)
{
if (this.wrapped is { } w)
return w.TryLock(out errorMessage);
errorMessage = nameof(ObjectDisposedException);
return null;
}
public ILockedImFont Lock() =>
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
@ -876,7 +887,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
public void Pop() => this.WrappedNotDisposed.Pop();
public Task<IFontHandle> WaitAsync() =>
this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this);
this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this)
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
public Task<IFontHandle> WaitAsync(CancellationToken cancellationToken) =>
this.wrapped?.WaitAsync(cancellationToken)
.ContinueWith(_ => (IFontHandle)this, cancellationToken)
?? Task.FromException<IFontHandle>(new ObjectDisposedException(nameof(FontHandleWrapper)));
public override string ToString() =>
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";

View file

@ -0,0 +1,92 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Utility;
/// <summary>Wrapper aroundx <see cref="ImDrawData"/> containing one <see cref="ImDrawList"/>.</summary>
public unsafe struct BufferBackedImDrawData : IDisposable
{
private nint buffer;
/// <summary>Initializes a new instance of the <see cref="BufferBackedImDrawData"/> struct.</summary>
/// <param name="buffer">Address of buffer to use.</param>
private BufferBackedImDrawData(nint buffer) => this.buffer = buffer;
/// <summary>Gets the <see cref="ImDrawData"/> stored in this buffer.</summary>
public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data;
/// <summary>Gets the <see cref="ImDrawDataPtr"/> stored in this buffer.</summary>
public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data));
/// <summary>Gets the <see cref="ImDrawList"/> stored in this buffer.</summary>
public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List;
/// <summary>Gets the <see cref="ImDrawListPtr"/> stored in this buffer.</summary>
public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List));
/// <summary>Creates a new instance of <see cref="BufferBackedImDrawData"/>.</summary>
/// <returns>A new instance of <see cref="BufferBackedImDrawData"/>.</returns>
public static BufferBackedImDrawData Create()
{
if (ImGui.GetCurrentContext().IsNull || ImGui.GetIO().FontDefault.Handle is null)
throw new("ImGui is not ready");
var res = new BufferBackedImDrawData(Marshal.AllocHGlobal(sizeof(DataStruct)));
var ds = (DataStruct*)res.buffer;
*ds = default;
var atlas = ImGui.GetIO().Fonts;
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID;
ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
ds->SharedData.Font = ImGui.GetIO().FontDefault;
ds->SharedData.FontSize = ds->SharedData.Font->FontSize;
ds->SharedData.ClipRectFullscreen = new(
float.NegativeInfinity,
float.NegativeInfinity,
float.PositiveInfinity,
float.PositiveInfinity);
ds->List.Data = &ds->SharedData;
ds->ListPtr = &ds->List;
ds->Data.CmdLists = &ds->ListPtr;
ds->Data.CmdListsCount = 1;
ds->Data.FramebufferScale = Vector2.One;
res.ListPtr._ResetForNewFrame();
res.ListPtr.PushClipRectFullScreen();
res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
return res;
}
/// <summary>Updates the statistics information stored in <see cref="DataPtr"/> from <see cref="ListPtr"/>.</summary>
public readonly void UpdateDrawDataStatistics()
{
this.Data.TotalIdxCount = this.List.IdxBuffer.Size;
this.Data.TotalVtxCount = this.List.VtxBuffer.Size;
}
/// <inheritdoc/>
public void Dispose()
{
if (this.buffer != 0)
{
this.ListPtr._ClearFreeMemory();
Marshal.FreeHGlobal(this.buffer);
this.buffer = 0;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct DataStruct
{
public ImDrawData Data;
public ImDrawList* ListPtr;
public ImDrawList List;
public ImDrawListSharedData SharedData;
}
}

View file

@ -575,6 +575,15 @@ public static partial class ImGuiHelpers
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.IsNull ? other : self;
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
/// <param name="sss">SeString to render.</param>
/// <param name="style">Initial rendering style.</param>
/// <returns>A new self-contained draw data.</returns>
internal static BufferBackedImDrawData CreateDrawData(
ReadOnlySpan<byte> sss,
scoped in SeStringDrawParams style = default) =>
Service<SeStringRenderer>.Get().CreateDrawData(sss, style);
/// <summary>
/// Mark 4K page as used, after adding a codepoint to a font.
/// </summary>

View file

@ -6,10 +6,12 @@ using System.Text;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Game;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Serilog;
@ -33,6 +35,14 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
this.interfaceManager.Draw += this.InterfaceManagerOnDraw;
}
private enum ContextMenuActionType
{
None,
SaveAsFile,
CopyToClipboard,
SendToTexWidget,
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
var textureManager = await Service<TextureManager>.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}";
ContextMenuActionType action;
BitmapCodecInfo? encoder;
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo?>(
var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>(
TaskCreationOptions.RunContinuationsAsynchronously);
Service<InterfaceManager>.Get().Draw += DrawChoices;
encoder = await tcs.Task;
(action, encoder) = await tcs.Task;
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")]
void DrawChoices()
@ -98,13 +109,20 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
}
if (ImGui.Selectable("Copy"u8))
tcs.TrySetResult(null);
tcs.TrySetResult((ContextMenuActionType.CopyToClipboard, null));
if (ImGui.Selectable("Send to TexWidget"u8))
tcs.TrySetResult((ContextMenuActionType.SendToTexWidget, null));
ImGui.Separator();
foreach (var encoder2 in encoders)
{
if (ImGui.Selectable(encoder2.Name))
tcs.TrySetResult(encoder2);
tcs.TrySetResult((ContextMenuActionType.SaveAsFile, encoder2));
}
ImGui.Separator();
const float previewImageWidth = 320;
var size = textureWrap.Size;
if (size.X > previewImageWidth)
@ -120,50 +138,68 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
}
}
if (encoder is null)
switch (action)
{
isCopy = true;
await textureManager.CopyToClipboardAsync(textureWrap, name, true);
}
else
{
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
case ContextMenuActionType.CopyToClipboard:
isCopy = true;
await textureManager.CopyToClipboardAsync(textureWrap, name, true);
break;
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
name + encoder.Extensions.First(),
encoder.Extensions.First(),
(ok, path2) =>
{
if (!ok)
tcs.SetCanceled();
else
tcs.SetResult(path2);
});
var path = await tcs.Task.ConfigureAwait(false);
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
var notif = Service<NotificationManager>.Get().AddNotification(
new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
case ContextMenuActionType.SendToTexWidget:
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
n.Notification.DismissNow();
};
var framework = await Service<Framework>.GetAsync();
var dalamudInterface = await Service<DalamudInterface>.GetAsync();
await framework.RunOnFrameworkThread(
() =>
{
var texWidget = dalamudInterface.GetDataWindowWidget<TexWidget>();
dalamudInterface.SetDataWindowWidget(texWidget);
texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource()));
});
break;
}
case ContextMenuActionType.SaveAsFile when encoder is not null:
{
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
name + encoder.Extensions.First(),
encoder.Extensions.First(),
(ok, path2) =>
{
if (!ok)
tcs.SetCanceled();
else
tcs.SetResult(path2);
});
var path = await tcs.Task.ConfigureAwait(false);
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
var notif = Service<NotificationManager>.Get().AddNotification(
new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
n.Notification.DismissNow();
};
break;
}
}
}
catch (Exception e)

View file

@ -1,9 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CheapLoc;
@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Windowing;
/// <summary>
@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing;
public abstract class Window
{
private const float FadeInOutTime = 0.072f;
private const string AdditionsPopupName = "WindowSystemContextActions";
private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false;
private readonly TitleBarButton additionsButton;
private readonly List<TitleBarButton> allButtons = [];
private bool internalLastIsOpen = false;
private bool internalIsOpen = false;
private bool internalIsPinned = false;
@ -72,6 +76,20 @@ public abstract class Window
this.WindowName = name;
this.Flags = flags;
this.ForceMainWindow = forceMainWindow;
this.additionsButton = new()
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(AdditionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
}
/// <summary>
@ -425,8 +443,17 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
this.PreDraw();
this.ApplyConditionals();
var isErrorStylePushed = false;
if (!this.hasError)
{
this.PreDraw();
this.ApplyConditionals();
}
else
{
Style.StyleModelV1.DalamudStandard.Push();
isErrorStylePushed = true;
}
if (this.ForceMainWindow)
ImGuiHelpers.ForceNextWindowMainViewport();
@ -448,10 +475,22 @@ public abstract class Window
var flags = this.Flags;
if (this.internalIsPinned || this.internalIsClickthrough)
{
flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
}
if (this.internalIsClickthrough)
{
flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
}
// If we have an error, reset all flags to default, and unlock window size.
if (this.hasError)
{
flags = ImGuiWindowFlags.None;
ImGui.SetNextWindowCollapsed(false, ImGuiCond.Once);
ImGui.SetNextWindowSizeConstraints(Vector2.Zero, Vector2.PositiveInfinity);
}
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{
@ -461,14 +500,12 @@ public abstract class Window
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
}
// Not supported yet on non-main viewports
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
this.internalAlpha = null;
this.internalIsPinned = false;
this.internalIsClickthrough = false;
this.presetDirty = true;
if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
else
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
}
if (this.hasError)
@ -492,7 +529,6 @@ public abstract class Window
}
}
const string additionsPopupName = "WindowSystemContextActions";
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
@ -503,13 +539,8 @@ public abstract class Window
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
{
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
if (!isAvailable)
ImGui.BeginDisabled();
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@ -557,21 +588,11 @@ public abstract class Window
this.presetDirty = true;
}
if (isAvailable)
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionViewportDisclaimer",
"These features are only available if this window is inside the game window."));
}
if (!isAvailable)
ImGui.EndDisabled();
ImGui.TextColored(
ImGuiColors.DalamudGrey,
Loc.Localize(
"WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough."));
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
@ -582,34 +603,15 @@ public abstract class Window
ImGui.PopStyleVar();
}
unsafe
if (flagsApplicableForTitleBarIcons)
{
var window = ImGuiP.GetCurrentWindow();
ImRect outRect;
ImGuiP.TitleBarRect(&outRect, window);
var additionsButton = new TitleBarButton
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
if (flagsApplicableForTitleBarIcons)
{
this.DrawTitleBarButtons(window, flags, outRect,
showAdditions
? this.TitleBarButtons.Append(additionsButton)
: this.TitleBarButtons);
}
this.allButtons.Clear();
this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1);
this.allButtons.AddRange(this.TitleBarButtons);
if (showAdditions)
this.allButtons.Add(this.additionsButton);
this.allButtons.Sort(static (a, b) => b.Priority - a.Priority);
this.DrawTitleBarButtons();
}
if (wasFocused)
@ -670,7 +672,17 @@ public abstract class Window
Task.FromResult<IDalamudTextureWrap>(tex));
}
this.PostDraw();
if (!this.hasError)
{
this.PostDraw();
}
else
{
if (isErrorStylePushed)
{
Style.StyleModelV1.DalamudStandard.Pop();
}
}
this.PostHandlePreset(persistence);
@ -766,8 +778,11 @@ public abstract class Window
}
}
private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable<TitleBarButton> buttons)
private unsafe void DrawTitleBarButtons()
{
var window = ImGuiP.GetCurrentWindow();
var flags = window.Flags;
var titleBarRect = window.TitleBarRect();
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle();
@ -802,26 +817,22 @@ public abstract class Window
var max = pos + new Vector2(fontSize, fontSize);
ImRect bb = new(pos, max);
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
bool hovered, held;
var pressed = false;
bool hovered, held, pressed;
if (this.internalIsClickthrough)
{
hovered = false;
held = false;
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
if (ImGui.IsMouseHoveringRect(pos, max))
{
hovered = true;
var pad = ImGui.GetStyle().TouchExtraPadding;
var rect = new ImRect(pos - pad, max + pad);
hovered = rect.Contains(ImGui.GetMousePos());
// We can't use ImGui native functions here, because they don't work with clickthrough
if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
{
held = true;
pressed = true;
}
}
// Temporarily enable inputs
// This will be reset on next frame, and then enabled again if it is still being hovered
if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
// We can't use ImGui native functions here, because they don't work with clickthrough
pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0;
}
else
{
@ -850,7 +861,7 @@ public abstract class Window
return pressed;
}
foreach (var button in buttons.OrderBy(x => x.Priority))
foreach (var button in this.allButtons)
{
if (this.internalIsClickthrough && !button.AvailableClickthrough)
return;
@ -1004,7 +1015,7 @@ public abstract class Window
/// <summary>
/// Gets or sets an action that is called when the button is clicked.
/// </summary>
public Action<ImGuiMouseButton> Click { get; set; }
public Action<ImGuiMouseButton>? Click { get; set; }
/// <summary>
/// Gets or sets the priority the button shall be shown in.

View file

@ -79,7 +79,7 @@ public interface IPlayerState : IDalamudService
bool IsLevelSynced { get; }
/// <summary>
/// Gets the effective level of the local character.
/// Gets the effective level of the local character, taking level sync into account.
/// </summary>
short EffectiveLevel { get; }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
@ -186,6 +187,17 @@ public interface ITextureProvider : IDalamudService
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Creates a texture by drawing a SeString onto it.</summary>
/// <param name="text">SeString to render.</param>
/// <param name="drawParams">Parameters for drawing.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <returns>The new texture.</returns>
/// <remarks>Can be only be used from the main thread.</remarks>
public IDalamudTextureWrap CreateTextureFromSeString(
ReadOnlySpan<byte> text,
scoped in SeStringDrawParams drawParams = default,
string? debugName = null);
/// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns>
/// <remarks>

View file

@ -1,7 +1,12 @@
using System.Runtime.CompilerServices;
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dalamud.Bindings.ImGui;
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
public unsafe struct ImVector
{
public readonly int Size;
@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data;
}
public ref T Ref<T>(int index)
{
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
}
public readonly ref T Ref<T>(int index) => ref Unsafe.AsRef<T>((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
public IntPtr Address<T>(int index)
{
return (IntPtr)((byte*)Data + index * Unsafe.SizeOf<T>());
}
public readonly nint Address<T>(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
}
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
/// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam>
public unsafe struct ImVector<T> where T : unmanaged
[StructLayout(LayoutKind.Sequential)]
public unsafe struct ImVector<T> : IEnumerable<T>
where T : unmanaged
{
private int size;
private int capacity;
private T* data;
/// <summary>
/// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer.
/// </summary>
@ -45,11 +50,6 @@ public unsafe struct ImVector<T> where T : unmanaged
this.data = data;
}
private int size;
private int capacity;
private unsafe T* data;
/// <summary>
/// Gets or sets the element at the specified index.
/// </summary>
@ -58,80 +58,72 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception>
public T this[int index]
{
get
readonly get
{
if (index < 0 || index >= size)
{
if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
}
return data[index];
return this.data[index];
}
set
{
if (index < 0 || index >= size)
{
if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
}
data[index] = value;
this.data[index] = value;
}
}
/// <summary>
/// Gets a pointer to the first element of the vector.
/// </summary>
public readonly T* Data => data;
public readonly T* Data => this.data;
/// <summary>
/// Gets a pointer to the first element of the vector.
/// </summary>
public readonly T* Front => data;
public readonly T* Front => this.data;
/// <summary>
/// Gets a pointer to the last element of the vector.
/// </summary>
public readonly T* Back => size > 0 ? data + size - 1 : null;
public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
/// <summary>
/// Gets or sets the capacity of the vector.
/// </summary>
public int Capacity
{
readonly get => capacity;
readonly get => this.capacity;
set
{
if (capacity == value)
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
if (this.capacity == value)
return;
}
if (data == null)
if (this.data == null)
{
data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
}
else
{
int newSize = Math.Min(size, value);
T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(data);
data = newData;
size = newSize;
var newSize = Math.Min(this.size, value);
var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(this.data);
this.data = newData;
this.size = newSize;
}
capacity = value;
this.capacity = value;
// Clear the rest of the data
for (int i = size; i < capacity; i++)
{
data[i] = default;
}
new Span<T>(this.data + this.size, this.capacity - this.size).Clear();
}
}
/// <summary>
/// Gets the number of elements in the vector.
/// </summary>
public readonly int Size => size;
public readonly int Size => this.size;
/// <summary>
/// Grows the capacity of the vector to at least the specified value.
@ -139,10 +131,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="newCapacity">The new capacity.</param>
public void Grow(int newCapacity)
{
if (newCapacity > capacity)
{
Capacity = newCapacity * 2;
}
var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
}
/// <summary>
@ -151,10 +141,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="size">The minimum capacity required.</param>
public void EnsureCapacity(int size)
{
if (size > capacity)
{
if (size > this.capacity)
Grow(size);
}
}
/// <summary>
@ -164,25 +152,46 @@ public unsafe struct ImVector<T> where T : unmanaged
public void Resize(int newSize)
{
EnsureCapacity(newSize);
size = newSize;
this.size = newSize;
}
/// <summary>
/// Clears all elements from the vector.
/// </summary>
public void Clear()
public void Clear() => this.size = 0;
/// <summary>
/// Adds an element to the end of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
[OverloadResolutionPriority(1)]
public void PushBack(T value)
{
size = 0;
this.EnsureCapacity(this.size + 1);
this.data[this.size++] = value;
}
/// <summary>
/// Adds an element to the end of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
public void PushBack(T value)
[OverloadResolutionPriority(2)]
public void PushBack(in T value)
{
EnsureCapacity(size + 1);
data[size++] = value;
EnsureCapacity(this.size + 1);
this.data[this.size++] = value;
}
/// <summary>
/// Adds an element to the front of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
public void PushFront(in T value)
{
if (this.size == 0)
this.PushBack(value);
else
this.Insert(0, value);
}
/// <summary>
@ -190,48 +199,126 @@ public unsafe struct ImVector<T> where T : unmanaged
/// </summary>
public void PopBack()
{
if (size > 0)
if (this.size > 0)
{
size--;
this.size--;
}
}
public ref T Insert(int index, in T v) {
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + 1);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + 1,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
this.data[index] = v;
this.size++;
return ref this.data[index];
}
public Span<T> InsertRange(int index, ReadOnlySpan<T> v)
{
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + v.Length);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + v.Length,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
var dstSpan = new Span<T>(this.data + index, v.Length);
v.CopyTo(new(this.data + index, v.Length));
this.size += v.Length;
return dstSpan;
}
/// <summary>
/// Frees the memory allocated for the vector.
/// </summary>
public void Free()
{
if (data != null)
if (this.data != null)
{
ImGui.MemFree(data);
data = null;
size = 0;
capacity = 0;
ImGui.MemFree(this.data);
this.data = null;
this.size = 0;
this.capacity = 0;
}
}
public ref T Ref(int index)
public readonly ref T Ref(int index)
{
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
return ref Unsafe.AsRef<T>((byte*)Data + (index * Unsafe.SizeOf<T>()));
}
public ref TCast Ref<TCast>(int index)
public readonly ref TCast Ref<TCast>(int index)
{
return ref Unsafe.AsRef<TCast>((byte*)Data + index * Unsafe.SizeOf<TCast>());
return ref Unsafe.AsRef<TCast>((byte*)Data + (index * Unsafe.SizeOf<TCast>()));
}
public void* Address(int index)
public readonly void* Address(int index)
{
return (byte*)Data + index * Unsafe.SizeOf<T>();
return (byte*)Data + (index * Unsafe.SizeOf<T>());
}
public void* Address<TCast>(int index)
public readonly void* Address<TCast>(int index)
{
return (byte*)Data + index * Unsafe.SizeOf<TCast>();
return (byte*)Data + (index * Unsafe.SizeOf<TCast>());
}
public ImVector* ToUntyped()
public readonly ImVector* ToUntyped()
{
return (ImVector*)Unsafe.AsPointer(ref this);
return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this));
}
public readonly Span<T> AsSpan() => new(this.data, this.size);
public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public struct Enumerator(T* begin, T* end) : IEnumerator<T>, IEnumerable<T>
{
private T* current = null;
public readonly ref T Current => ref *this.current;
readonly T IEnumerator<T>.Current => this.Current;
readonly object IEnumerator.Current => this.Current;
public bool MoveNext()
{
var next = this.current == null ? begin : this.current + 1;
if (next == end)
return false;
this.current = next;
return true;
}
public void Reset() => this.current = null;
public readonly Enumerator GetEnumerator() => new(begin, end);
readonly void IDisposable.Dispose()
{
}
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}

@ -1 +1 @@
Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77
Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f

@ -1 +1 @@
Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c