From 483630d1e6e02f064ef8be3ac0196aef6288ef3c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 29 Jul 2024 15:32:39 +0900 Subject: [PATCH] Add link support --- .../Internal/SeStringRenderer.cs | 350 +++++++++++++++--- .../SeStringRenderStyle.cs | 25 ++ .../Widgets/SeStringRendererTestWidget.cs | 43 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 30 +- Dalamud/Interface/Utility/ImGuiId.cs | 107 ++++++ 5 files changed, 481 insertions(+), 74 deletions(-) create mode 100644 Dalamud/Interface/Utility/ImGuiId.cs diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 78e3affa6c..8314cfea67 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -31,9 +31,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal; internal unsafe class SeStringRenderer : IInternalDisposableService { private const int ChannelShadow = 0; - private const int ChannelEdge = 1; - private const int ChannelFore = 2; - private const int ChannelCount = 3; + private const int ChannelLinkBackground = 1; + private const int ChannelLinkUnderline = 2; + private const int ChannelEdge = 3; + private const int ChannelFore = 4; + private const int ChannelCount = 5; private const int ImGuiContextCurrentWindowOffset = 0x3FF0; private const int ImGuiWindowDcOffset = 0x118; @@ -50,7 +52,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService private readonly uint[] colorTypes; private readonly uint[] edgeColorTypes; - private readonly List words = []; + private readonly List fragments = []; private readonly List colorStack = []; private readonly List edgeColorStack = []; @@ -99,31 +101,52 @@ public ReadOnlySeString Compile(string text) => this.cache.GetOrAdd( /// Creates and caches a SeString from a text macro representation, and then draws it. /// SeString text macro representation. /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. - public void CompileAndDrawWrapped(string text, SeStringRenderStyle style = default, float wrapWidth = 0) + /// Byte offset of the link payload that is being hovered, or -1 if none, and whether that link + /// (or the text itself if no link is active) is clicked. + public (int ByteOffset, bool Clicked) CompileAndDrawWrapped( + string text, + SeStringRenderStyle style = default, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault, + float wrapWidth = 0) { ThreadSafety.AssertMainThread(); - this.DrawWrapped(this.Compile(text).AsSpan(), style, wrapWidth); + return this.DrawWrapped(this.Compile(text).AsSpan(), style, imGuiId, buttonFlags, wrapWidth); } - /// - public void DrawWrapped(in Utf8String utf8String, SeStringRenderStyle style = default, float wrapWidth = 0) => - this.DrawWrapped(utf8String.AsSpan(), style, wrapWidth); + /// + public (int ByteOffset, bool Clicked) DrawWrapped( + in Utf8String utf8String, + SeStringRenderStyle style = default, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault, float wrapWidth = 0) => + this.DrawWrapped(utf8String.AsSpan(), style, imGuiId, buttonFlags, wrapWidth); /// Draws a SeString. /// SeString to draw. /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. - public void DrawWrapped(ReadOnlySeStringSpan sss, SeStringRenderStyle style = default, float wrapWidth = 0) + /// Byte offset of the link payload that is being hovered, or -1 if none, and whether that link + /// (or the text itself if no link is active) is clicked. + public (int ByteOffset, bool Clicked) DrawWrapped( + ReadOnlySeStringSpan sss, + SeStringRenderStyle style = default, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault, float wrapWidth = 0) { ThreadSafety.AssertMainThread(); if (wrapWidth <= 0) wrapWidth = ImGui.GetContentRegionAvail().X; - this.words.Clear(); + this.fragments.Clear(); this.colorStack.Clear(); this.edgeColorStack.Clear(); this.shadowColorStack.Clear(); @@ -140,38 +163,99 @@ public void DrawWrapped(ReadOnlySeStringSpan sss, SeStringRenderStyle style = de var pCurrentWindow = *(nint*)(ImGui.GetCurrentContext() + ImGuiContextCurrentWindowOffset); var pWindowDc = pCurrentWindow + ImGuiWindowDcOffset; var currLineTextBaseOffset = *(float*)(pWindowDc + ImGuiWindowTempDataCurrLineTextBaseOffset); - + this.CreateTextFragments(ref state, currLineTextBaseOffset, wrapWidth); var size = Vector2.Zero; - for (var i = 0; i < this.words.Count; i++) + var hoveredLinkOffset = -1; + var activeLinkOffset = -1; + for (var i = 0; i < this.fragments.Count; i++) { - var word = this.words[i]; - this.DrawWord( + var fragment = this.fragments[i]; + this.DrawTextFragment( ref state, - word.Offset, - state.Raw.Data[word.From..word.To], + fragment.Offset, + state.Raw.Data[fragment.From..fragment.To], i == 0 ? '\0' - : this.words[i - 1].IsSoftHyphenVisible - ? this.words[i - 1].LastRuneRepr - : this.words[i - 1].LastRuneRepr2); + : this.fragments[i - 1].IsSoftHyphenVisible + ? this.fragments[i - 1].LastRuneRepr + : this.fragments[i - 1].LastRuneRepr2, + fragment.ActiveLinkOffset); - if (word.IsSoftHyphenVisible && i > 0) + if (fragment.IsSoftHyphenVisible && i > 0) { - this.DrawWord( + this.DrawTextFragment( ref state, - word.Offset + new Vector2(word.AdvanceWidthWithoutLastRune, 0), + fragment.Offset + new Vector2(fragment.AdvanceWidthWithoutLastRune, 0), "-"u8, - this.words[i - 1].LastRuneRepr); + this.fragments[i - 1].LastRuneRepr, + fragment.ActiveLinkOffset); } - size = Vector2.Max(size, word.Offset + new Vector2(word.VisibleWidth, state.FontSize)); + size = Vector2.Max(size, fragment.Offset + new Vector2(fragment.VisibleWidth, state.FontSize)); + } + + ImGui.Dummy(size); + + var clicked = false; + var invisibleButtonDrawn = false; + foreach (var fragment in this.fragments) + { + var fragmentSize = new Vector2(fragment.AdvanceWidth, state.FontSize); + if (fragment.ActiveLinkOffset != -1) + { + var pos = ImGui.GetMousePos() - state.ScreenOffset - fragment.Offset; + if (pos is { X: >= 0, Y: >= 0 } && pos.X <= fragmentSize.X && pos.Y <= fragmentSize.Y) + { + invisibleButtonDrawn = true; + + var cursorPosBackup = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(state.ScreenOffset + fragment.Offset); + var pushed = imGuiId.PushId(); + clicked = ImGui.InvisibleButton("##link", new(fragment.AdvanceWidth, state.FontSize)); + if (ImGui.IsItemHovered()) + hoveredLinkOffset = fragment.ActiveLinkOffset; + if (ImGui.IsItemActive()) + activeLinkOffset = fragment.ActiveLinkOffset; + if (pushed) + ImGui.PopID(); + ImGui.SetCursorScreenPos(cursorPosBackup); + + break; + } + } + + size = Vector2.Max(size, fragmentSize); + } + + if (!invisibleButtonDrawn) + { + ImGui.SetCursorScreenPos(state.ScreenOffset); + clicked = ImGui.InvisibleButton("##text", size); + } + + if (hoveredLinkOffset != -1 || activeLinkOffset != -1) + { + state.SetCurrentChannel(ChannelLinkBackground); + foreach (var f in this.fragments) + { + if (f.ActiveLinkOffset != hoveredLinkOffset && hoveredLinkOffset != -1) + continue; + if (f.ActiveLinkOffset != activeLinkOffset && activeLinkOffset != -1) + continue; + state.DrawList.AddRectFilled( + state.ScreenOffset + f.Offset, + state.ScreenOffset + f.Offset + new Vector2(f.AdvanceWidth, state.FontSize), + activeLinkOffset == -1 + ? this.currentStyle.LinkHoverColor + : this.currentStyle.LinkActiveColor); + } } state.Splitter.Merge(state.DrawList); - ImGui.Dummy(size); + return (hoveredLinkOffset, clicked); } /// Gets the printable char for the given char, or null(\0) if it should not be handled at all. @@ -208,52 +292,102 @@ private void CreateTextFragments(ref DrawState state, float baseOffset, float wr var prev = 0; var runningOffset = new Vector2(0, baseOffset); var runningWidth = 0f; - foreach (var (curr, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString)) + var activeLinkOffset = -1; + foreach (var (curr2, mandatory) in new LineBreakEnumerator(state.Raw, UtfEnumeratorFlags.Utf8SeString)) { - var fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset); + var curr = curr2; + var fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset, activeLinkOffset); var nextRunningWidth = Math.Max(runningWidth, runningOffset.X + fragment.VisibleWidth); + var nextLinkOffset = activeLinkOffset; if (nextRunningWidth <= wrapWidth) { // New fragment fits in the current line. - if (this.words.Count > 0) + foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..curr2]).GetOffsetEnumerator()) { - char lastFragmentEnd; - if (this.words[^1].EndsWithSoftHyphen) - { - runningOffset.X += this.words[^1].AdvanceWidthWithoutLastRune - this.words[^1].AdvanceWidth; - lastFragmentEnd = this.words[^1].LastRuneRepr; - } - else + if (p.Payload.MacroCode == MacroCode.Link) { - lastFragmentEnd = this.words[^1].LastRuneRepr2; - } + nextLinkOffset = + p.Payload.TryGetExpression(out var e) && + e.TryGetUInt(out var u) && + u == (uint)LinkMacroPayloadType.Terminator + ? -1 + : prev + p.Offset; + if (p.Offset != 0) + { + curr = prev + p.Offset; + this.CreateNonBreakingTextFragment( + ref state, + ref runningOffset, + ref prev, + curr, + curr == curr2 && mandatory, + activeLinkOffset); + } - runningOffset.X += MathF.Round( - state.Font.GetDistanceAdjustmentForPair(lastFragmentEnd, fragment.FirstRuneRepr) * - state.FontSizeScale); - fragment = fragment with { Offset = runningOffset }; + activeLinkOffset = nextLinkOffset; + } } - this.words.Add(fragment); + this.CreateNonBreakingTextFragment( + ref state, + ref runningOffset, + ref prev, + curr2, + mandatory, + activeLinkOffset); + + prev = curr2; runningWidth = nextRunningWidth; - runningOffset.X += fragment.AdvanceWidth; - prev = curr; } else if (fragment.VisibleWidth <= wrapWidth) { // New fragment does not fit in the current line, but it will fit in the next line. // Implicit conditions: runningWidth > 0, this.words.Count > 0 - runningWidth = fragment.VisibleWidth; - runningOffset.X = fragment.AdvanceWidth; + runningOffset.X = 0; runningOffset.Y += state.FontSize; - prev = curr; - this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true }; - this.words.Add(fragment with { Offset = runningOffset with { X = 0 } }); + this.fragments[^1] = this.fragments[^1] with { MandatoryBreakAfter = true }; + + foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..curr2]).GetOffsetEnumerator()) + { + if (p.Payload.MacroCode == MacroCode.Link) + { + nextLinkOffset = + p.Payload.TryGetExpression(out var e) && + e.TryGetUInt(out var u) && + u == (uint)LinkMacroPayloadType.Terminator + ? -1 + : prev + p.Offset; + if (p.Offset != 0) + { + curr = prev + p.Offset; + this.CreateNonBreakingTextFragment( + ref state, + ref runningOffset, + ref prev, + curr, + curr == curr2 && mandatory, + activeLinkOffset); + } + + activeLinkOffset = nextLinkOffset; + } + } + + this.CreateNonBreakingTextFragment( + ref state, + ref runningOffset, + ref prev, + curr2, + mandatory, + activeLinkOffset); + + prev = curr2; + runningWidth = this.fragments[^1].AdvanceWidth; } else { // New fragment does not fit in the given width, and it needs to be broken down. - while (prev < curr) + while (prev < curr2) { if (runningOffset.X > 0) { @@ -261,13 +395,40 @@ private void CreateTextFragments(ref DrawState state, float baseOffset, float wr runningOffset.Y += state.FontSize; } - fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset, wrapWidth); + curr = curr2; + foreach (var p in new ReadOnlySeStringSpan(state.Raw.Data[prev..curr2]).GetOffsetEnumerator()) + { + if (p.Payload.MacroCode == MacroCode.Link) + { + nextLinkOffset = + p.Payload.TryGetExpression(out var e) && + e.TryGetUInt(out var u) && + u == (uint)LinkMacroPayloadType.Terminator + ? -1 + : prev + p.Offset; + if (p.Offset != 0) + { + curr = prev + p.Offset; + break; + } + } + } + + fragment = state.CreateFragment( + this, + prev, + curr, + fragment.To != curr || mandatory, + runningOffset, + activeLinkOffset, + wrapWidth); + activeLinkOffset = nextLinkOffset; runningWidth = fragment.VisibleWidth; runningOffset.X = fragment.AdvanceWidth; prev = fragment.To; - if (this.words.Count > 0) - this.words[^1] = this.words[^1] with { MandatoryBreakAfter = true }; - this.words.Add(fragment); + if (this.fragments.Count > 0) + this.fragments[^1] = this.fragments[^1] with { MandatoryBreakAfter = true }; + this.fragments.Add(fragment); } } @@ -279,7 +440,46 @@ private void CreateTextFragments(ref DrawState state, float baseOffset, float wr } } - private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan span, char lastRuneRepr) + private void CreateNonBreakingTextFragment( + ref DrawState state, + ref Vector2 runningOffset, + ref int prev, + int curr, + bool mandatory, + int activeLinkOffset) + { + var fragment = state.CreateFragment(this, prev, curr, mandatory, runningOffset, activeLinkOffset); + + if (this.fragments.Count > 0) + { + char lastFragmentEnd; + if (this.fragments[^1].EndsWithSoftHyphen) + { + runningOffset.X += this.fragments[^1].AdvanceWidthWithoutLastRune - this.fragments[^1].AdvanceWidth; + lastFragmentEnd = this.fragments[^1].LastRuneRepr; + } + else + { + lastFragmentEnd = this.fragments[^1].LastRuneRepr2; + } + + runningOffset.X += MathF.Round( + state.Font.GetDistanceAdjustmentForPair(lastFragmentEnd, fragment.FirstRuneRepr) * + state.FontSizeScale); + fragment = fragment with { Offset = runningOffset }; + } + + this.fragments.Add(fragment); + runningOffset.X += fragment.AdvanceWidth; + prev = curr; + } + + private void DrawTextFragment( + ref DrawState state, + Vector2 offset, + ReadOnlySpan span, + char lastRuneRepr, + int activeLinkOffset) { var gfdTextureSrv = (nint)UIModule.Instance()->GetRaptureAtkModule()->AtkModule.AtkFontManager.Gfd->Texture-> @@ -288,13 +488,15 @@ private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan sp var width = 0f; foreach (var c in UtfEnumerator.From(span, UtfEnumeratorFlags.Utf8SeString)) { + var activeColor = this.colorStack.Count == 0 ? this.currentStyle.Color : this.colorStack[^1]; + if (c.IsSeStringPayload) { - var enu = new ReadOnlySeStringSpan(span[c.ByteOffset..]).GetEnumerator(); + var enu = new ReadOnlySeStringSpan(span[c.ByteOffset..]).GetOffsetEnumerator(); if (!enu.MoveNext()) continue; - var payload = enu.Current; + var payload = enu.Current.Payload; switch (payload.MacroCode) { case MacroCode.Color: @@ -343,6 +545,19 @@ private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan sp Vector2.Zero, useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0, useHq ? gfdEntry.HqUv1 : gfdEntry.Uv1); + if (activeLinkOffset != -1 && this.currentStyle.LinkUnderline) + { + state.SetCurrentChannel(ChannelLinkUnderline); + state.DrawList.AddLine( + state.ScreenOffset + offset + new Vector2( + x, + MathF.Round(state.Font.Ascent * state.FontSizeScale)), + state.ScreenOffset + offset + new Vector2( + x + size.X, + MathF.Round(state.Font.Ascent * state.FontSizeScale)), + activeColor); + } + width = Math.Max(width, x + size.X); x += MathF.Round(size.X); lastRuneRepr = '\0'; @@ -400,10 +615,22 @@ private void DrawWord(ref DrawState state, Vector2 offset, ReadOnlySpan sp } state.SetCurrentChannel(ChannelFore); - var activeColor = this.colorStack.Count == 0 ? this.currentStyle.Color : this.colorStack[^1]; for (var dx = this.currentStyle.Bold ? 1 : 0; dx >= 0; dx--) state.Draw(offset + new Vector2(x + dist + dx, 0), g, dyItalic, activeColor); + if (activeLinkOffset != -1 && this.currentStyle.LinkUnderline) + { + state.SetCurrentChannel(ChannelLinkUnderline); + state.DrawList.AddLine( + state.ScreenOffset + offset + new Vector2( + x + dist, + MathF.Round(state.Font.Ascent * state.FontSizeScale)), + state.ScreenOffset + offset + new Vector2( + x + dist + g.AdvanceX, + MathF.Round(state.Font.Ascent * state.FontSizeScale)), + activeColor); + } + width = Math.Max(width, x + dist + (g.X1 * state.FontSizeScale)); x += dist + MathF.Round(g.AdvanceX * state.FontSizeScale); } @@ -522,6 +749,7 @@ when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId): private readonly record struct TextFragment( int From, int To, + int ActiveLinkOffset, Vector2 Offset, float VisibleWidth, float AdvanceWidth, @@ -615,7 +843,8 @@ public TextFragment CreateFragment( int to, bool mandatoryBreakAfter, Vector2 offset, - float wrapWidth = float.PositiveInfinity) + int activeLinkOffset, + float wrapWidth = float.MaxValue) { var lastNonSpace = from; @@ -690,6 +919,7 @@ public TextFragment CreateFragment( return new( from, to, + activeLinkOffset, offset, visibleWidth, advanceWidth, diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringRenderStyle.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringRenderStyle.cs index f5516f3584..25110760b2 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringRenderStyle.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringRenderStyle.cs @@ -10,9 +10,13 @@ public record struct SeStringRenderStyle private uint color; private uint edgeColor; private uint shadowColor; + private uint linkHoverColor; + private uint linkActiveColor; private bool hasValidColorValue; private bool hasValidEdgeColorValue; private bool hasValidShadowColorValue; + private bool hasValidLinkHoverColorValue; + private bool hasValidLinkActiveColorValue; /// Gets or sets a value indicating whether to force the color of the rendered text edge. /// If not null, then and @@ -31,6 +35,9 @@ public record struct SeStringRenderStyle /// Gets or sets a value indicating whether the text is rendered with shadow. public bool Shadow { get; set; } + /// Gets or sets a value indicating whether to underline links. + public bool LinkUnderline { get; set; } + /// Gets or sets the color of the rendered text. public uint Color { @@ -51,4 +58,22 @@ public uint ShadowColor readonly get => this.hasValidShadowColorValue ? this.shadowColor : 0xFF000000; set => (this.hasValidShadowColorValue, this.shadowColor) = (true, value); } + + /// Gets or sets the background color of a link when hovered. + public uint LinkHoverColor + { + readonly get => this.hasValidLinkHoverColorValue + ? this.linkHoverColor + : ImGui.GetColorU32(ImGuiCol.ButtonHovered); + set => (this.hasValidLinkHoverColorValue, this.linkHoverColor) = (true, value); + } + + /// Gets or sets the background color of a link when active. + public uint LinkActiveColor + { + readonly get => this.hasValidLinkActiveColorValue + ? this.linkActiveColor + : ImGui.GetColorU32(ImGuiCol.ButtonActive); + set => (this.hasValidLinkActiveColorValue, this.linkActiveColor) = (true, value); + } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 46b15b09e4..241a2ba609 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -44,10 +44,35 @@ public void Load() /// public void Draw() { + var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color); + if (ImGui.ColorEdit4("Color", ref t2)) + this.style.Color = ImGui.ColorConvertFloat4ToU32(t2); + + t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor); + if (ImGui.ColorEdit4("Edge Color", ref t2)) + this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2); + + ImGui.SameLine(); var t = this.style.ForceEdgeColor; - if (ImGui.Checkbox("Force Edge Color", ref t)) + if (ImGui.Checkbox("Forced", ref t)) this.style.ForceEdgeColor = t; + t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor); + if (ImGui.ColorEdit4("Shadow Color", ref t2)) + this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2); + + t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverColor); + if (ImGui.ColorEdit4("Link Hover Color", ref t2)) + this.style.LinkHoverColor = ImGui.ColorConvertFloat4ToU32(t2); + + t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveColor); + if (ImGui.ColorEdit4("Link Active Color", ref t2)) + this.style.LinkActiveColor = ImGui.ColorConvertFloat4ToU32(t2); + + t = this.style.Edge; + if (ImGui.Checkbox("Edge", ref t)) + this.style.Edge = t; + ImGui.SameLine(); t = this.style.Bold; if (ImGui.Checkbox("Bold", ref t)) @@ -58,16 +83,16 @@ public void Draw() if (ImGui.Checkbox("Italic", ref t)) this.style.Italic = t; - ImGui.SameLine(); - t = this.style.Edge; - if (ImGui.Checkbox("Edge", ref t)) - this.style.Edge = t; - ImGui.SameLine(); t = this.style.Shadow; if (ImGui.Checkbox("Shadow", ref t)) this.style.Shadow = t; + ImGui.SameLine(); + t = this.style.LinkUnderline; + if (ImGui.Checkbox("Link Underline", ref t)) + this.style.LinkUnderline = t; + if (ImGui.CollapsingHeader("Addon Table")) { this.addons ??= Service.Get().GetExcelSheet()!.ToArray(); @@ -116,7 +141,7 @@ public void Draw() { this.testStringBuffer.Dispose(); this.testStringBuffer = ImVectorWrapper.CreateFromSpan( - "
Lorem ipsum dolor sit amet, conse<->ctetur adipi<->scing elit. Maece<->nas digni<->ssim sem at inter<->dum ferme<->ntum. Praes<->ent ferme<->ntum conva<->llis velit sit amet hendr<->erit. Sed eu nibh magna. Integ<->er nec lacus in velit porta euism<->od sed et lacus. Sed non mauri<->s venen<->atis, matti<->s metus in, aliqu<->et dolor. Aliqu<->am erat volut<->pat. Nulla venen<->atis velit ac susci<->pit euism<->od. suspe<->ndisse maxim<->us viver<->ra dui id dapib<->us. Nam torto<->r dolor, eleme<->ntum quis orci id, pulvi<->nar fring<->illa quam. Pelle<->ntesque laore<->et viver<->ra torto<->r eget matti<->s. Vesti<->bulum eget porta ante, a molli<->s nulla. Curab<->itur a ligul<->a leo. Aliqu<->am volut<->pat sagit<->tis dapib<->us.\n\nFusce iacul<->is aliqu<->am mi, eget portt<->itor arcu solli<->citudin conse<->ctetur. suspe<->ndisse aliqu<->am commo<->do tinci<->dunt. Duis sed posue<->re tellu<->s. Sed phare<->tra ex vel torto<->r pelle<->ntesque, inter<->dum porta sapie<->n digni<->ssim. Queue Dun Scait<->h. Cras aliqu<->et at nulla quis moles<->tie. Vesti<->bulum eu ligul<->a sapie<->n. Curab<->itur digni<->ssim feugi<->at volut<->pat.\n\nVesti<->bulum condi<->mentum laore<->et rhonc<->us. Vivam<->us et accum<->san purus. Curab<->itur inter<->dum vel ligul<->a ac euism<->od. Donec sed nisl digni<->ssim est tinci<->dunt iacul<->is. Praes<->ent hendr<->erit pelle<->ntesque nisl, quis lacin<->ia arcu dictu<->m sit amet. Aliqu<->am variu<->s lectu<->s vel mauri<->s imper<->diet posue<->re. Ut gravi<->da non sapie<->n sed hendr<->erit.\n\nProin quis dapib<->us odio. Cras sagit<->tis non sem sed porta. Donec iacul<->is est ligul<->a, digni<->ssim aliqu<->et augue matti<->s vitae. Duis ullam<->corper tempu<->s odio, non vesti<->bulum est biben<->dum quis. In purus elit, vehic<->ula tinci<->dunt dictu<->m in, aucto<->r nec enim. Curab<->itur a nisi in leo matti<->s pelle<->ntesque id nec sem. Nunc vel ultri<->ces nisl. Nam congu<->e vulpu<->tate males<->uada. Aenea<->n vesti<->bulum mauri<->s leo, sit amet iacul<->is est imper<->diet ut. Phase<->llus nec lobor<->tis lacus, sit amet scele<->risque purus. Nam id lacin<->ia velit, euism<->od feugi<->at dui. Nulla sodal<->es odio ligul<->a, et hendr<->erit torto<->r maxim<->us eu. Donec et sem eu magna volut<->pat accum<->san non ut lectu<->s.\n\nVivam<->us susci<->pit ferme<->ntum gravi<->da. Cras nec conse<->ctetur magna. Vivam<->us ante massa, accum<->san sit amet felis et, tempu<->s iacul<->is ipsum. Pelle<->ntesque vitae nisi accum<->san, venen<->atis lectu<->s aucto<->r, aliqu<->et liber<->o. Nam nec imper<->diet justo. Vivam<->us ut vehic<->ula turpi<->s. Nunc lobor<->tis pelle<->ntesque urna, sit amet solli<->citudin nibh fauci<->bus in. Curab<->itur eu lobor<->tis lacus. Donec eu hendr<->erit diam, vitae cursu<->s odio. Cras eget scele<->risque mi.


colortype502,edgecolortype503\nopacity FF\nopacity 80\nopacity 00\nTest 1\nTest 2\nWithout edgeShadowWith edge"u8, + "
Lorem ipsum dolor sit amet, conse<->ctetur adipi<->scing elit. Maece<->nas digni<->ssim sem at inter<->dum ferme<->ntum. Praes<->ent ferme<->ntum conva<->llis velit sit amet hendr<->erit. Sed eu nibh magna. Integ<->er nec lacus in velit porta euism<->od sed et lacus. Sed non mauri<->s venen<->atis, matti<->s metus in, aliqu<->et dolor. Aliqu<->am erat volut<->pat. Nulla venen<->atis velit ac susci<->pit euism<->od. suspe<->ndisse maxim<->us viver<->ra dui id dapib<->us. Nam torto<->r dolor, eleme<->ntum quis orci id, pulvi<->nar fring<->illa quam. Pelle<->ntesque laore<->et viver<->ra torto<->r eget matti<->s. Vesti<->bulum eget porta ante, a molli<->s nulla. Curab<->itur a ligul<->a leo. Aliqu<->am volut<->pat sagit<->tis dapib<->us.\n\nFusce iacul<->is aliqu<->am mi, eget portt<->itor arcu solli<->citudin conse<->ctetur. suspe<->ndisse aliqu<->am commo<->do tinci<->dunt. Duis sed posue<->re tellu<->s. Sed phare<->tra ex vel torto<->r pelle<->ntesque, inter<->dum porta sapie<->n digni<->ssim. Queue Dun Scait<->h. Cras aliqu<->et at nulla quis moles<->tie. Vesti<->bulum eu ligul<->a sapie<->n. Curab<->itur digni<->ssim feugi<->at volut<->pat.\n\nVesti<->bulum condi<->mentum laore<->et rhonc<->us. Vivam<->us et accum<->san purus. Curab<->itur inter<->dum vel ligul<->a ac euism<->od. Donec sed nisl digni<->ssim est tinci<->dunt iacul<->is. Praes<->ent hendr<->erit pelle<->ntesque nisl, quis lacin<->ia arcu dictu<->m sit amet. Aliqu<->am variu<->s lectu<->s vel mauri<->s imper<->diet posue<->re. Ut gravi<->da non sapie<->n sed hendr<->erit.\n\nProin quis dapib<->us odio. Cras sagit<->tis non sem sed porta. Donec iacul<->is est ligul<->a, digni<->ssim aliqu<->et augue matti<->s vitae. Duis ullam<->corper tempu<->s odio, non vesti<->bulum est biben<->dum quis. In purus elit, vehic<->ula tinci<->dunt dictu<->m in, aucto<->r nec enim. Curab<->itur a nisi in leo matti<->s pelle<->ntesque id nec sem. Nunc vel ultri<->ces nisl. Nam congu<->e vulpu<->tate males<->uada. Aenea<->n vesti<->bulum mauri<->s leo, sit amet iacul<->is est imper<->diet ut. Phase<->llus nec lobor<->tis lacus, sit amet scele<->risque purus. Nam id lacin<->ia velit, euism<->od feugi<->at dui. Nulla sodal<->es odio ligul<->a, et hendr<->erit torto<->r maxim<->us eu. Donec et sem eu magna volut<->pat accum<->san non ut lectu<->s.\n\nVivam<->us susci<->pit ferme<->ntum gravi<->da. Cras nec conse<->ctetur magna. Vivam<->us ante massa, accum<->san sit amet felis et, tempu<->s iacul<->is ipsum. Pelle<->ntesque vitae nisi accum<->san, venen<->atis lectu<->s aucto<->r, aliqu<->et liber<->o. Nam nec imper<->diet justo. Vivam<->us ut vehic<->ula turpi<->s. Nunc lobor<->tis pelle<->ntesque urna, sit amet solli<->citudin nibh fauci<->bus in. Curab<->itur eu lobor<->tis lacus. Donec eu hendr<->erit diam, vitae cursu<->s odio. Cras eget scele<->risque mi.
\n\nTesting aaaaalink aaaaabbbb.\n\n

colortype502,edgecolortype503\n\nopacity FF\nopacity 80\nopacity 00\nTest 1\nTest 2\nWithout edgeShadowWith edge"u8, minCapacity: 65536); this.testString = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan); } @@ -153,6 +178,8 @@ public void Draw() } ImGui.Separator(); - ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style); + var test = ImGuiHelpers.CompileSeStringWrapped(this.testString, this.style); + + ImGui.TextUnformatted($"Hovered: {test.ByteOffset}\nClicked: {test.Clicked}"); } } diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index b042caaeba..f7f22456da 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -184,30 +184,48 @@ public static void ClickToCopyText(string text, string? textCopy = null) /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. public static void SeStringWrapped(ReadOnlySpan sss, float wrapWidth = 0) => - Service.Get().DrawWrapped(sss, default, wrapWidth); + Service.Get().DrawWrapped(sss, wrapWidth: wrapWidth); /// Creates and caches a SeString from a text macro representation, and then draws it. /// SeString text macro representation. /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. public static void CompileSeStringWrapped(string text, float wrapWidth = 0) => - Service.Get().CompileAndDrawWrapped(text, default, wrapWidth); + Service.Get().CompileAndDrawWrapped(text, wrapWidth: wrapWidth); /// Draws a SeString. /// SeString to draw. /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. - public static void SeStringWrapped(ReadOnlySpan sss, SeStringRenderStyle style, float wrapWidth = 0) => - Service.Get().DrawWrapped(sss, style, wrapWidth); + /// Byte offset of the link payload that is being hovered, or -1 if none, and whether that link + /// (or the text itself if no link is active) is clicked. + public static (int ByteOffset, bool Clicked) SeStringWrapped( + ReadOnlySpan sss, + SeStringRenderStyle style = default, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault, + float wrapWidth = 0) => + Service.Get().DrawWrapped(sss, style, imGuiId, buttonFlags, wrapWidth); /// Creates and caches a SeString from a text macro representation, and then draws it. /// SeString text macro representation. /// Initial rendering style. + /// ImGui ID, if link functionality is desired. + /// Button flags to use on link interaction. /// Wrapping width. If a non-positive number is provided, then the remainder of the width /// will be used. - public static void CompileSeStringWrapped(string text, SeStringRenderStyle style, float wrapWidth = 0) => - Service.Get().CompileAndDrawWrapped(text, style, wrapWidth); + /// Byte offset of the link payload that is being hovered, or -1 if none, and whether that link + /// (or the text itself if no link is active) is clicked. + public static (int ByteOffset, bool Clicked) CompileSeStringWrapped( + string text, + SeStringRenderStyle style, + ImGuiId imGuiId = default, + ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault, + float wrapWidth = 0) => + Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags, wrapWidth); /// /// Write unformatted text wrapped. diff --git a/Dalamud/Interface/Utility/ImGuiId.cs b/Dalamud/Interface/Utility/ImGuiId.cs new file mode 100644 index 0000000000..f198bec753 --- /dev/null +++ b/Dalamud/Interface/Utility/ImGuiId.cs @@ -0,0 +1,107 @@ +using ImGuiNET; + +namespace Dalamud.Interface.Utility; + +/// Represents any type of ImGui ID. +public readonly ref struct ImGuiId +{ + /// Type of the ID. + public readonly Type IdType; + + /// Numeric ID. Valid if is . + public readonly nint Numeric; + + /// UTF-16 string ID. Valid if is . + public readonly ReadOnlySpan U16; + + /// UTF-8 string ID. Valid if is . + public readonly ReadOnlySpan U8; + + /// Initializes a new instance of the struct. + /// A numeric ID, or 0 to not provide an ID. + public ImGuiId(nint id) + { + if (id != 0) + (this.IdType, this.Numeric) = (Type.Numeric, id); + } + + /// Initializes a new instance of the struct. + /// A UTF-16 string ID, or to not provide an ID. + public ImGuiId(ReadOnlySpan id) + { + if (!id.IsEmpty) + { + this.IdType = Type.U16; + this.U16 = id; + } + } + + /// Initializes a new instance of the struct. + /// A UTF-8 string ID, or to not provide an ID. + public ImGuiId(ReadOnlySpan id) + { + if (!id.IsEmpty) + { + this.IdType = Type.U8; + this.U8 = id; + } + } + + /// Possible types for an ImGui ID. + public enum Type + { + /// No ID is specified. + None, + + /// field is used. + Numeric, + + /// field is used. + U16, + + /// field is used. + U8, + } + + public static implicit operator ImGuiId(nint id) => new(id); + + public static implicit operator ImGuiId(ReadOnlySpan id) => new(id); + + public static implicit operator ImGuiId(ReadOnlySpan id) => new(id); + + public static implicit operator bool(ImGuiId id) => !id.IsEmpty(); + + /// Determines if no ID is stored. + /// true if no ID is stored. + public bool IsEmpty() => this.IdType switch + { + Type.None => true, + Type.Numeric => this.Numeric == 0, + Type.U16 => this.U16.IsEmpty, + Type.U8 => this.U8.IsEmpty, + _ => true, + }; + + /// Pushes ID if any is stored. + /// true if any ID is pushed. + public unsafe bool PushId() + { + switch (this.IdType) + { + case Type.Numeric: + ImGuiNative.igPushID_Ptr((void*)this.Numeric); + return true; + case Type.U16: + fixed (void* p = this.U16) + ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + (this.U16.Length * 2)); + return true; + case Type.U8: + fixed (void* p = this.U8) + ImGuiNative.igPushID_StrStr((byte*)p, (byte*)p + this.U8.Length); + return true; + case Type.None: + default: + return false; + } + } +}