From c19ea6ace38f79386f1c4727851dcbcdf23b036f Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Tue, 1 Jul 2025 19:02:32 +0900
Subject: [PATCH 1/9] Add ITextureProvider.CreateTextureFromSeString
---
.../Internal/SeStringColorStackSet.cs | 32 ++--
.../Internal/SeStringRenderer.cs | 176 +++++++++---------
.../Internal/TextFragment.cs | 39 ++++
.../SeStringDrawState.cs | 99 +++++++---
.../Interface/Internal/DalamudInterface.cs | 7 +
.../Internal/Windows/Data/DataWindow.cs | 31 ++-
.../Data/Widgets/FontAwesomeTestWidget.cs | 26 ++-
.../Widgets/SeStringRendererTestWidget.cs | 46 +++--
.../Windows/Data/Widgets/TexWidget.cs | 12 +-
.../Interface/ManagedFontAtlas/IFontHandle.cs | 24 ++-
.../ManagedFontAtlas/Internals/FontHandle.cs | 7 +-
.../Internal/TextureManager.FromSeString.cs | 35 ++++
.../Textures/Internal/TextureManager.cs | 3 +-
.../Internal/TextureManagerPluginScoped.cs | 13 ++
Dalamud/Interface/UiBuilder.cs | 33 +++-
.../Utility/BufferBackedImDrawData.cs | 112 +++++++++++
Dalamud/Interface/Utility/ImGuiHelpers.cs | 9 +
.../Utility/Internal/DevTextureSaveMenu.cs | 126 ++++++++-----
Dalamud/Plugin/Services/ITextureProvider.cs | 14 +-
19 files changed, 629 insertions(+), 215 deletions(-)
create mode 100644 Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
create mode 100644 Dalamud/Interface/Utility/BufferBackedImDrawData.cs
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
index ad60d405e..85ab2e441 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
@@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// Color stacks to use while evaluating a SeString.
internal sealed class SeStringColorStackSet
{
- /// Parsed , containing colors to use with and
- /// .
- private readonly uint[,] colorTypes;
-
/// Foreground color stack while evaluating a SeString for rendering.
/// Touched only from the main thread.
private readonly List 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(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1)))
+ foreach (ref var r in new Span(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r);
}
}
}
+ /// Initializes a new instance of the class.
+ /// Color types.
+ public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
+
/// Gets a value indicating whether at least one color has been pushed to the edge color stack.
public bool HasAdditionalEdgeColor { get; private set; }
+ /// Gets the parsed containing colors to use with
+ /// and .
+ public uint[,] ColorTypes { get; }
+
/// Resets the colors in the stack.
/// Draw state.
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);
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index d0c40cd9f..0099e6e5d 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -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;
/// Draws SeString.
[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
/// Parsed text fragments from a SeString.
/// Touched only from the main thread.
- private readonly List fragments = [];
+ private readonly List fragmentsMainThread = [];
/// Color stacks to use while evaluating a SeString for rendering.
/// Touched only from the main thread.
- private readonly SeStringColorStackSet colorStackSet;
-
- /// Splits a draw list so that different layers of a single glyph can be drawn out of order.
- private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
+ private readonly SeStringColorStackSet colorStackSetMainThread;
[ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{
- this.colorStackSet = new(dm.Excel.GetSheet());
+ this.colorStackSetMainThread = new(dm.Excel.GetSheet());
this.gfd = dm.GetFile("common/font/gfdata.gfd")!;
}
- /// Finalizes an instance of the class.
- ~SeStringRenderer() => this.ReleaseUnmanagedResources();
-
- ///
- void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
-
/// Compiles and caches a SeString from a text macro representation.
/// SeString text macro representation.
/// Newline characters will be normalized to newline payloads.
@@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
text.ReplaceLineEndings("
"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Parameters for drawing.
+ /// A new self-contained draw data.
+ 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(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;
+ }
+ }
+
/// Compiles and caches a SeString from a text macro representation, and then draws it.
/// SeString text macro representation.
/// Newline characters will be normalized to newline payloads.
@@ -113,28 +143,42 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// ImGui ID, if link functionality is desired.
/// Button flags to use on link interaction.
/// Interaction result of the rendered text.
- 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.
+ using 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;
- }
- }
-
/// Creates text fragment, taking line and word breaking into account.
/// Draw state.
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
/// Determines a bitmap icon to display for the given SeString payload.
/// Byte span that should include a SeString payload.
/// Icon to display, or if it should not be displayed as an icon.
- private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan sss)
+ private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan 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);
}
-
- /// Represents a text fragment in a SeString span.
- /// Starting byte offset (inclusive) in a SeString.
- /// Ending byte offset (exclusive) in a SeString.
- /// Byte offset of the link that decorates this text fragment, or -1 if none.
- /// Offset in pixels w.r.t. .
- /// Replacement entity, if any.
- /// Visible width of this text fragment. This is the width required to draw everything
- /// without clipping.
- /// Advance width of this text fragment. This is the width required to add to the cursor
- /// to position the next fragment correctly.
- /// Same with , but trimming all the
- /// trailing soft hyphens.
- /// Whether to insert a line break after this text fragment.
- /// Whether this text fragment ends with one or more soft hyphens.
- /// First rune in this text fragment.
- /// Last rune in this text fragment, for the purpose of calculating kerning distance with
- /// the following text fragment in the same line, if any.
- 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;
- }
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
new file mode 100644
index 000000000..a64c32109
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
@@ -0,0 +1,39 @@
+using System.Numerics;
+using System.Text;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
+
+/// Represents a text fragment in a SeString span.
+/// Starting byte offset (inclusive) in a SeString.
+/// Ending byte offset (exclusive) in a SeString.
+/// Byte offset of the link that decorates this text fragment, or -1 if none.
+/// Offset in pixels w.r.t. .
+/// Replacement entity, if any.
+/// Visible width of this text fragment. This is the width required to draw everything
+/// without clipping.
+/// Advance width of this text fragment. This is the width required to add to the cursor
+/// to position the next fragment correctly.
+/// Same with , but trimming all the
+/// trailing soft hyphens.
+/// Whether to insert a line break after this text fragment.
+/// Whether this text fragment ends with one or more soft hyphens.
+/// First rune in this text fragment.
+/// Last rune in this text fragment, for the purpose of calculating kerning distance with
+/// the following text fragment in the same line, if any.
+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)
+{
+ /// Gets a value indicating whether the fragment ends with a visible soft hyphen.
+ public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 64a7f3db3..722de1fda 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -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;
@@ -14,51 +17,80 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// Calculated values from using ImGui styles.
[StructLayout(LayoutKind.Sequential)]
-public unsafe ref struct SeStringDrawState
+public unsafe ref struct SeStringDrawState : IDisposable
{
private static readonly int ChannelCount = Enum.GetValues().Length;
private readonly ImDrawList* drawList;
- private readonly SeStringColorStackSet colorStackSet;
- private readonly ImDrawListSplitter* splitter;
+
+ private ImDrawListSplitter splitter;
/// Initializes a new instance of the struct.
/// Raw SeString byte span.
/// Instance of to initialize from.
/// Instance of to use.
- /// Instance of ImGui Splitter to use.
+ /// Fragments.
+ /// Font to use.
internal SeStringDrawState(
ReadOnlySpan span,
scoped in SeStringDrawParams ssdp,
SeStringColorStackSet colorStackSet,
- ImDrawListSplitter* splitter)
+ List 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();
}
///
@@ -135,7 +167,7 @@ public unsafe ref struct SeStringDrawState
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawEdge =>
- (this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
+ (this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
@@ -143,11 +175,21 @@ public unsafe ref struct SeStringDrawState
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
+ /// Gets the color stacks.
+ internal SeStringColorStackSet ColorStackSet { get; }
+
+ /// Gets the text fragments.
+ internal List Fragments { get; }
+
+ ///
+ public void Dispose() =>
+ ImGuiNative.Destroy((ImDrawListSplitter*)Unsafe.AsPointer(ref this.splitter));
+
/// Sets the current channel in the ImGui draw list splitter.
/// Channel to switch to.
[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);
/// Draws a single texture.
/// ImGui texture ID to draw from.
@@ -216,7 +258,7 @@ public unsafe ref struct SeStringDrawState
/// Draws a single glyph using current styling configurations.
/// Glyph to draw.
/// Offset of the glyph in pixels w.r.t. .
- 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(g.TextureIndex).TexID;
var xy0 = new Vector2(
@@ -268,7 +310,7 @@ public unsafe ref struct SeStringDrawState
/// Offset of the glyph in pixels w.r.t.
/// .
/// Advance width of the glyph.
- internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
+ internal void DrawLinkUnderline(Vector2 offset, float advanceWidth)
{
if (this.LinkUnderlineThickness < 1f)
return;
@@ -350,15 +392,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 +421,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 +435,9 @@ public unsafe ref struct SeStringDrawState
/// Splits the draw list.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal readonly void SplitDrawList() =>
- this.splitter->Split(this.drawList, ChannelCount);
+ internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
/// Merges the draw list.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList);
+ internal void MergeDrawList() => this.splitter.Merge(this.drawList);
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index d475d36bc..7afe7e709 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -531,6 +531,13 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Restart();
}
+ ///
+ public T GetDataWindowWidget() where T : IDataWindowWidget => this.dataWindow.GetWidget();
+
+ /// Sets the data window current widget.
+ /// Widget to set current.
+ public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
+
private void OnDraw()
{
this.FrameCount++;
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index ae86958dd..eb0589d59 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept;
private bool selectionCollapsed;
- private IDataWindowWidget currentWidget;
+
private bool isLoaded;
///
@@ -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();
}
+ /// Gets or sets the current widget.
+ public IDataWindowWidget CurrentWidget { get; set; }
+
///
public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose();
@@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable
{
}
+ /// Gets the data window widget of the specified type.
+ /// Type of the data window widget to find.
+ /// Found widget.
+ public T GetWidget() 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.");
+ }
+
///
/// Set the DataKind dropdown menu.
///
@@ -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
{
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
index 91f1af98e..4f5540daf 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
@@ -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.Get().ShowTextureSaveMenuAsync(
+ this.DisplayName,
+ this.icons[i].ToString(),
+ Task.FromResult(
+ Service.Get().CreateTextureFromSeString(
+ ReadOnlySeString.FromText(this.icons[i].ToIconString()),
+ new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() })));
+ }
+
ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f);
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index 7ff5a63be..0f51e0322 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -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
///
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.Get().CompileAndCache(this.testString).Data.Span));
}
+ ImGui.SameLine();
+
+ if (ImGui.Button("Copy as Image"))
+ {
+ _ = Service.Get().ShowTextureSaveMenuAsync(
+ this.DisplayName,
+ $"From {nameof(SeStringRendererTestWidget)}",
+ Task.FromResult(
+ Service.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:
" +
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
index 52fa0e822..3416a2506 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
@@ -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();
}
+ /// Adds a texture wrap for debug display purposes.
+ /// Task returning a texture.
+ public void AddTexture(Task textureTask) => this.addedTextures.Add(new(Api10: textureTask));
+
private unsafe void DrawBlame(List allBlames)
{
var im = Service.Get();
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
index 2853aa4d2..be2f5a742 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
@@ -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
///
///
/// Use directly if you want to keep the current ImGui font if the font is not ready.
- /// Alternatively, use to wait for this property to become true.
+ /// Alternatively, use to wait for this property to become true.
///
bool Available { get; }
+ ///
+ /// Attempts to lock the fully constructed instance of corresponding to the this
+ /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
+ ///
+ /// The error message, if any.
+ ///
+ /// An instance of that must be disposed after use on success;
+ /// null with populated on failure.
+ ///
+ ILockedImFont? TryLock(out string? errorMessage);
+
///
/// Locks the fully constructed instance of corresponding to the this
/// , for use in any thread.
@@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable
///
/// A task containing this .
Task WaitAsync();
+
+ ///
+ /// Waits for to become true.
+ ///
+ /// The cancellation token.
+ /// A task containing this .
+ Task WaitAsync(CancellationToken cancellationToken);
}
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
index 1fdaf4596..98a823deb 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
@@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle
}
///
- public Task WaitAsync()
+ public Task WaitAsync() => this.WaitAsync(CancellationToken.None);
+
+ ///
+ public Task WaitAsync(CancellationToken cancellationToken)
{
if (this.Available)
return Task.FromResult(this);
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ cancellationToken.Register(() => tcs.TrySetCanceled());
+
this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed;
if (this.Available)
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
new file mode 100644
index 000000000..3e90ae3ea
--- /dev/null
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
@@ -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;
+
+/// Service responsible for loading and disposing ImGui texture wraps.
+internal sealed partial class TextureManager
+{
+ [ServiceManager.ServiceDependency]
+ private readonly SeStringRenderer seStringRenderer = Service.Get();
+
+ ///
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan 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;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs
index 059c716ce..d0f0d8c07 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs
@@ -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()
{
diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
index 9b0fa0943..ac6de7dd7 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
@@ -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;
}
+ ///
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan 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;
+ }
+
///
public IEnumerable GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index b870e7a94..355b5d571 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
@@ -657,13 +658,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true,
string? debugName = null) =>
- this.scopedFinalizer.Add(Service
- .Get()
- .CreateFontAtlas(
- this.namespaceName + ":" + (debugName ?? "custom"),
- autoRebuildMode,
- isGlobalScaled,
- this.plugin));
+ this.scopedFinalizer.Add(
+ Service
+ .Get()
+ .CreateFontAtlas(
+ this.namespaceName + ":" + (debugName ?? "custom"),
+ autoRebuildMode,
+ isGlobalScaled,
+ this.plugin));
///
/// Unregister the UiBuilder. Do not call this in plugin code.
@@ -825,6 +827,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));
@@ -833,7 +844,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
public void Pop() => this.WrappedNotDisposed.Pop();
public Task WaitAsync() =>
- this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this);
+ this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this)
+ ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper)));
+
+ public Task WaitAsync(CancellationToken cancellationToken) =>
+ this.wrapped?.WaitAsync(cancellationToken)
+ .ContinueWith(_ => (IFontHandle)this, cancellationToken)
+ ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper)));
public override string ToString() =>
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";
diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
new file mode 100644
index 000000000..112fda8a8
--- /dev/null
+++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
@@ -0,0 +1,112 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Bindings.ImGui;
+
+namespace Dalamud.Interface.Utility;
+
+/// Wrapper aroundx containing one .
+public unsafe struct BufferBackedImDrawData : IDisposable
+{
+ private nint buffer;
+
+ /// Initializes a new instance of the struct.
+ /// Address of buffer to use.
+ private BufferBackedImDrawData(nint buffer) => this.buffer = buffer;
+
+ /// Gets the stored in this buffer.
+ public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data;
+
+ /// Gets the stored in this buffer.
+ public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data));
+
+ /// Gets the stored in this buffer.
+ public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List;
+
+ /// Gets the stored in this buffer.
+ public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List));
+
+ /// Creates a new instance of .
+ /// A new instance of .
+ 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;
+ ref var atlasTail = ref ImFontAtlasTailReal.From(atlas);
+ ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
+ ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.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(atlasTail.TextureIndexCommon));
+ return res;
+ }
+
+ /// Updates the statistics information stored in from .
+ public readonly void UpdateDrawDataStatistics()
+ {
+ this.Data.TotalIdxCount = this.List.IdxBuffer.Size;
+ this.Data.TotalVtxCount = this.List.VtxBuffer.Size;
+ }
+
+ ///
+ 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;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ImFontAtlasTailReal
+ {
+ /// Index of texture containing the below.
+ public int TextureIndexCommon;
+
+ /// Custom texture rectangle ID for both of the below.
+ public int PackIdCommon;
+
+ /// Custom texture rectangle for white pixel and mouse cursors.
+ public ImFontAtlasCustomRect RectMouseCursors;
+
+ /// Custom texture rectangle for baked anti-aliased lines.
+ public ImFontAtlasCustomRect RectLines;
+
+ public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) =>
+ ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint));
+ }
+}
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index 27cb3596c..98159c1bc 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -234,6 +234,15 @@ public static partial class ImGuiHelpers
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Initial rendering style.
+ /// A new self-contained draw data.
+ public static BufferBackedImDrawData CreateDrawData(
+ ReadOnlySpan sss,
+ scoped in SeStringDrawParams style = default) =>
+ Service.Get().CreateDrawData(sss, style);
+
///
/// Write unformatted text wrapped.
///
diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
index 86435e8c1..4a0137c88 100644
--- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
+++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
@@ -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,
+ }
+
///
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
@@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
var textureManager = await Service.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(
+ var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>(
TaskCreationOptions.RunContinuationsAsynchronously);
Service.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();
- 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(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.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.GetAsync();
+ var dalamudInterface = await Service.GetAsync();
+ await framework.RunOnFrameworkThread(
+ () =>
+ {
+ var texWidget = dalamudInterface.GetDataWindowWidget();
+ dalamudInterface.SetDataWindowWidget(texWidget);
+ texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource()));
+ });
+ break;
+ }
+
+ case ContextMenuActionType.SaveAsFile when encoder is not null:
+ {
+ var props = new Dictionary();
+ 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(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.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)
diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs
index a8ad76995..9c499d3f5 100644
--- a/Dalamud/Plugin/Services/ITextureProvider.cs
+++ b/Dalamud/Plugin/Services/ITextureProvider.cs
@@ -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
string? debugName = null,
CancellationToken cancellationToken = default);
+ /// Creates a texture by drawing a SeString onto it.
+ /// SeString to render.
+ /// Parameters for drawing.
+ /// Name for debug display purposes.
+ /// The new texture.
+ /// Can be only be used from the main thread.
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan text,
+ scoped in SeStringDrawParams drawParams = default,
+ string? debugName = null);
+
/// Gets the supported bitmap decoders.
/// The supported bitmap decoders.
///
From 40e63f2d9a7e015f1c7659f714d6aff1c3397918 Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Mon, 11 Aug 2025 00:44:02 +0900
Subject: [PATCH 2/9] Enable viewport alpha
---
Dalamud/Interface/Windowing/Window.cs | 21 +++++++++------------
lib/cimgui | 2 +-
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index d302552f5..a6b5e0801 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -449,11 +449,8 @@ public abstract class Window
}
// Not supported yet on non-main viewports
- if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
- ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
+ if (this.internalIsClickthrough && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
- this.internalAlpha = null;
- this.internalIsPinned = false;
this.internalIsClickthrough = false;
this.presetDirty = true;
}
@@ -482,11 +479,6 @@ public abstract class Window
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
{
- var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
-
- if (!isAvailable)
- ImGui.BeginDisabled();
-
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@@ -506,6 +498,11 @@ public abstract class Window
if (this.internalIsClickthrough)
ImGui.EndDisabled();
+ var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
+
+ if (!isAvailable)
+ ImGui.BeginDisabled();
+
if (this.AllowClickthrough)
{
if (ImGui.Checkbox(
@@ -519,6 +516,9 @@ public abstract class Window
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
+ if (!isAvailable)
+ ImGui.EndDisabled();
+
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
@@ -547,9 +547,6 @@ public abstract class Window
"These features are only available if this window is inside the game window."));
}
- if (!isAvailable)
- ImGui.EndDisabled();
-
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
diff --git a/lib/cimgui b/lib/cimgui
index 27c8565f6..68cce5e21 160000
--- a/lib/cimgui
+++ b/lib/cimgui
@@ -1 +1 @@
-Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
+Subproject commit 68cce5e2185948612eb80d981d4001b9737c32cf
From e5451c37af8dc2f9b24c6bee35c2416e6a65b3a0 Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Tue, 12 Aug 2025 16:18:49 +0900
Subject: [PATCH 3/9] Update InputHandler to match changes in
imgui_impl_win32.cpp
---
.../InputHandler/Win32InputHandler.cs | 255 ++++++++++--------
.../Internal/Windows/TitleScreenMenuWindow.cs | 3 +-
imgui/Dalamud.Bindings.ImGui/ImVector.cs | 237 ++++++++++------
3 files changed, 308 insertions(+), 187 deletions(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 596df4c67..62e254a1a 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -34,11 +34,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 +65,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 +76,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 +95,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);
-
///
public bool UpdateCursor { get; set; } = true;
@@ -155,6 +153,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 +167,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 +223,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 +269,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 +288,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 +303,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 +311,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 +405,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))
+ // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
+ if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{
- io.MousePos.X = pt.x;
- io.MousePos.Y = pt.y;
- }
- else
- {
- io.MousePos.X = float.MinValue;
- io.MousePos.Y = float.MinValue;
+ // 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 +500,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 +529,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 +695,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 +735,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() * 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() * 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)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)])]
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index e3eb22a04..69cdc4d28 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -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);
diff --git a/imgui/Dalamud.Bindings.ImGui/ImVector.cs b/imgui/Dalamud.Bindings.ImGui/ImVector.cs
index 9a10c1d6b..67e450193 100644
--- a/imgui/Dalamud.Bindings.ImGui/ImVector.cs
+++ b/imgui/Dalamud.Bindings.ImGui/ImVector.cs
@@ -1,7 +1,12 @@
-using System.Runtime.CompilerServices;
+using System.Collections;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace Dalamud.Bindings.ImGui;
+///
+/// A structure representing a dynamic array for unmanaged types.
+///
public unsafe struct ImVector
{
public readonly int Size;
@@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data;
}
- public ref T Ref(int index)
- {
- return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf());
- }
+ public readonly ref T Ref(int index) => ref Unsafe.AsRef((byte*)this.Data + (index * Unsafe.SizeOf()));
- public IntPtr Address(int index)
- {
- return (IntPtr)((byte*)Data + index * Unsafe.SizeOf());
- }
+ public readonly nint Address(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf()));
}
///
/// A structure representing a dynamic array for unmanaged types.
///
/// The type of elements in the vector, must be unmanaged.
-public unsafe struct ImVector where T : unmanaged
+[StructLayout(LayoutKind.Sequential)]
+public unsafe struct ImVector : IEnumerable
+ where T : unmanaged
{
+ private int size;
+ private int capacity;
+ private T* data;
+
///
/// Initializes a new instance of the struct with the specified size, capacity, and data pointer.
///
@@ -45,11 +50,6 @@ public unsafe struct ImVector where T : unmanaged
this.data = data;
}
- private int size;
- private int capacity;
- private unsafe T* data;
-
-
///
/// Gets or sets the element at the specified index.
///
@@ -58,80 +58,72 @@ public unsafe struct ImVector where T : unmanaged
/// Thrown when the index is out of range.
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;
}
}
///
/// Gets a pointer to the first element of the vector.
///
- public readonly T* Data => data;
+ public readonly T* Data => this.data;
///
/// Gets a pointer to the first element of the vector.
///
- public readonly T* Front => data;
+ public readonly T* Front => this.data;
///
/// Gets a pointer to the last element of the vector.
///
- public readonly T* Back => size > 0 ? data + size - 1 : null;
+ public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
///
/// Gets or sets the capacity of the vector.
///
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(this.data + this.size, this.capacity - this.size).Clear();
}
}
///
/// Gets the number of elements in the vector.
///
- public readonly int Size => size;
+ public readonly int Size => this.size;
///
/// Grows the capacity of the vector to at least the specified value.
@@ -139,10 +131,8 @@ public unsafe struct ImVector where T : unmanaged
/// The new capacity.
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;
}
///
@@ -151,10 +141,8 @@ public unsafe struct ImVector where T : unmanaged
/// The minimum capacity required.
public void EnsureCapacity(int size)
{
- if (size > capacity)
- {
+ if (size > this.capacity)
Grow(size);
- }
}
///
@@ -164,25 +152,46 @@ public unsafe struct ImVector where T : unmanaged
public void Resize(int newSize)
{
EnsureCapacity(newSize);
- size = newSize;
+ this.size = newSize;
}
///
/// Clears all elements from the vector.
///
- public void Clear()
+ public void Clear() => this.size = 0;
+
+ ///
+ /// Adds an element to the end of the vector.
+ ///
+ /// The value to add.
+ [OverloadResolutionPriority(1)]
+ public void PushBack(T value)
{
- size = 0;
+ this.EnsureCapacity(this.size + 1);
+ this.data[this.size++] = value;
}
///
/// Adds an element to the end of the vector.
///
/// The value to add.
- 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;
+ }
+
+ ///
+ /// Adds an element to the front of the vector.
+ ///
+ /// The value to add.
+ public void PushFront(in T value)
+ {
+ if (this.size == 0)
+ this.PushBack(value);
+ else
+ this.Insert(0, value);
}
///
@@ -190,48 +199,126 @@ public unsafe struct ImVector where T : unmanaged
///
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 InsertRange(int index, ReadOnlySpan 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(this.data + index, v.Length);
+ v.CopyTo(new(this.data + index, v.Length));
+ this.size += v.Length;
+ return dstSpan;
+ }
+
///
/// Frees the memory allocated for the vector.
///
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((byte*)Data + index * Unsafe.SizeOf());
+ return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf()));
}
- public ref TCast Ref(int index)
+ public readonly ref TCast Ref(int index)
{
- return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf());
+ return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf()));
}
- public void* Address(int index)
+ public readonly void* Address(int index)
{
- return (byte*)Data + index * Unsafe.SizeOf();
+ return (byte*)Data + (index * Unsafe.SizeOf());
}
- public void* Address(int index)
+ public readonly void* Address(int index)
{
- return (byte*)Data + index * Unsafe.SizeOf();
+ return (byte*)Data + (index * Unsafe.SizeOf());
}
- 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 AsSpan() => new(this.data, this.size);
+
+ public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ public struct Enumerator(T* begin, T* end) : IEnumerator, IEnumerable
+ {
+ private T* current = null;
+
+ public readonly ref T Current => ref *this.current;
+
+ readonly T IEnumerator.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 IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}
From 544f8b28bfc115968ea53fbfb08207c6d7d010ea Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Sat, 16 Aug 2025 16:42:30 +0900
Subject: [PATCH 4/9] Support make clickthrough
---
...Win32InputHandler.StaticLookupFunctions.cs | 9 +-
.../InputHandler/Win32InputHandler.cs | 28 ++--
Dalamud/Interface/Windowing/Window.cs | 133 ++++++++----------
3 files changed, 79 insertions(+), 91 deletions(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
index 5710a5991..a7b70ce35 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
@@ -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;
}
}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 62e254a1a..0b2e27b57 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -8,6 +8,7 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Memory;
+using Dalamud.Utility;
using Serilog;
@@ -446,19 +447,19 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
}
+ }
- // (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) 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.
@@ -827,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;
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index a6b5e0801..e24f96ff8 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -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;
///
@@ -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 allButtons = [];
+
private bool internalLastIsOpen = false;
private bool internalIsOpen = false;
private bool internalIsPinned = false;
@@ -69,6 +73,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,
+ };
}
///
@@ -448,11 +466,12 @@ public abstract class Window
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
}
- // Not supported yet on non-main viewports
- if (this.internalIsClickthrough && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
+ if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
- this.internalIsClickthrough = false;
- this.presetDirty = true;
+ if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
+ ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
+ else
+ ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
}
// Draw the actual window contents
@@ -466,7 +485,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) &&
@@ -477,7 +495,7 @@ public abstract class Window
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
- if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
+ if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
{
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@@ -498,11 +516,6 @@ public abstract class Window
if (this.internalIsClickthrough)
ImGui.EndDisabled();
- var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
-
- if (!isAvailable)
- ImGui.BeginDisabled();
-
if (this.AllowClickthrough)
{
if (ImGui.Checkbox(
@@ -516,9 +529,6 @@ public abstract class Window
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
- if (!isAvailable)
- ImGui.EndDisabled();
-
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
@@ -534,18 +544,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."));
- }
+ 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;
@@ -556,34 +559,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)
@@ -740,8 +724,11 @@ public abstract class Window
}
}
- private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable 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();
@@ -776,26 +763,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 ((global::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
{
@@ -824,7 +807,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;
@@ -932,7 +915,7 @@ public abstract class Window
///
/// Gets or sets an action that is called when the button is clicked.
///
- public Action Click { get; set; }
+ public Action? Click { get; set; }
///
/// Gets or sets the priority the button shall be shown in.
From 0e6dae9f6476050eaca0e01d2560bbbb136b123d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 3 Dec 2025 18:39:04 +0000
Subject: [PATCH 5/9] Update ClientStructs
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index e5f586630..e5dedba42 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77
+Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f
From df0bfc18c3877c027000b5400e78359e8b21b9f0 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:10:51 +0100
Subject: [PATCH 6/9] Make ImGuiHelpers.CreateDrawData() internal for now
---
Dalamud/Interface/Utility/ImGuiHelpers.cs | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index b8e7d5fe3..ee2840d3d 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -234,15 +234,6 @@ public static partial class ImGuiHelpers
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
- /// Creates a draw data that will draw the given SeString onto it.
- /// SeString to render.
- /// Initial rendering style.
- /// A new self-contained draw data.
- public static BufferBackedImDrawData CreateDrawData(
- ReadOnlySpan sss,
- scoped in SeStringDrawParams style = default) =>
- Service.Get().CreateDrawData(sss, style);
-
///
/// Write unformatted text wrapped.
///
@@ -584,6 +575,15 @@ public static partial class ImGuiHelpers
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.IsNull ? other : self;
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Initial rendering style.
+ /// A new self-contained draw data.
+ internal static BufferBackedImDrawData CreateDrawData(
+ ReadOnlySpan sss,
+ scoped in SeStringDrawParams style = default) =>
+ Service.Get().CreateDrawData(sss, style);
+
///
/// Mark 4K page as used, after adding a codepoint to a font.
///
From 1fe2d5412839378a446fdf2aea867eb66d731c78 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 01:29:04 +0100
Subject: [PATCH 7/9] Upgrade cimgui, prep for viewport alpha
---
lib/cimgui | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/cimgui b/lib/cimgui
index 27c8565f6..bc3272967 160000
--- a/lib/cimgui
+++ b/lib/cimgui
@@ -1 +1 @@
-Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
+Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c
From 9bce0d33a6deb5b1c8ca22cd53389a97c459dfdf Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 02:04:27 +0100
Subject: [PATCH 8/9] Don't try to free CLR memory
---
.../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +-
.../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +-----
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index 0099e6e5d..87df2da2c 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -168,7 +168,7 @@ internal class SeStringRenderer : IServiceType
// This also does argument validation for drawParams. Do it here.
// `using var` makes a struct read-only, but we do want to modify it.
- using var stateStorage = new SeStringDrawState(
+ var stateStorage = new SeStringDrawState(
sss,
drawParams,
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 722de1fda..11c1120b4 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -17,7 +17,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// Calculated values from using ImGui styles.
[StructLayout(LayoutKind.Sequential)]
-public unsafe ref struct SeStringDrawState : IDisposable
+public unsafe ref struct SeStringDrawState
{
private static readonly int ChannelCount = Enum.GetValues().Length;
@@ -181,10 +181,6 @@ public unsafe ref struct SeStringDrawState : IDisposable
/// Gets the text fragments.
internal List Fragments { get; }
- ///
- public void Dispose() =>
- ImGuiNative.Destroy((ImDrawListSplitter*)Unsafe.AsPointer(ref this.splitter));
-
/// Sets the current channel in the ImGui draw list splitter.
/// Channel to switch to.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
From 1b5fbaa82ed43343f73dfcf72a5941ba0ca16d60 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 02:04:45 +0100
Subject: [PATCH 9/9] Access custom font atlas fields directly through bindings
---
.../Utility/BufferBackedImDrawData.cs | 24 ++-----------------
1 file changed, 2 insertions(+), 22 deletions(-)
diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
index 112fda8a8..e6128992a 100644
--- a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
+++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
@@ -39,9 +39,8 @@ public unsafe struct BufferBackedImDrawData : IDisposable
*ds = default;
var atlas = ImGui.GetIO().Fonts;
- ref var atlasTail = ref ImFontAtlasTailReal.From(atlas);
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
- ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.TextureIndexCommon].TexID;
+ 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;
@@ -60,7 +59,7 @@ public unsafe struct BufferBackedImDrawData : IDisposable
res.ListPtr._ResetForNewFrame();
res.ListPtr.PushClipRectFullScreen();
- res.ListPtr.PushTextureID(new(atlasTail.TextureIndexCommon));
+ res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
return res;
}
@@ -90,23 +89,4 @@ public unsafe struct BufferBackedImDrawData : IDisposable
public ImDrawList List;
public ImDrawListSharedData SharedData;
}
-
- [StructLayout(LayoutKind.Sequential)]
- private struct ImFontAtlasTailReal
- {
- /// Index of texture containing the below.
- public int TextureIndexCommon;
-
- /// Custom texture rectangle ID for both of the below.
- public int PackIdCommon;
-
- /// Custom texture rectangle for white pixel and mouse cursors.
- public ImFontAtlasCustomRect RectMouseCursors;
-
- /// Custom texture rectangle for baked anti-aliased lines.
- public ImFontAtlasCustomRect RectLines;
-
- public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) =>
- ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint));
- }
}