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 64e1acaa4..af78c5b0c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -533,6 +533,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 e38537018..1ea0d9f2f 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; @@ -691,13 +692,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. @@ -868,6 +870,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder // Note: do not dispose w; we do not own it } + public ILockedImFont? TryLock(out string? errorMessage) + { + if (this.wrapped is { } w) + return w.TryLock(out errorMessage); + + errorMessage = nameof(ObjectDisposedException); + return null; + } + public ILockedImFont Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); @@ -876,7 +887,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public void Pop() => this.WrappedNotDisposed.Pop(); public Task 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 87f258250..ee2840d3d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -575,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. /// 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 7cd1b7c86..dc0522aa8 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 : IDalamudService 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. ///