From dea4ef4832d8d591c8b4cedb27e7ea5fe76400e6 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 1 Oct 2025 08:59:08 +0900 Subject: [PATCH] rich text, updated lightfinder description and bug fixes --- LightlessSync/UI/BroadcastUI.cs | 68 ++++++++++++++++++----- LightlessSync/UI/CompactUI.cs | 23 +++++--- LightlessSync/UI/EditProfileUi.cs | 31 ++++------- LightlessSync/UI/UISharedService.cs | 49 ++++++++++++++++- LightlessSync/Utils/SeStringUtils.cs | 82 +++++++++++++++++++++++++--- 5 files changed, 205 insertions(+), 48 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index eda5a53..dc0d740 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -1,10 +1,12 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; @@ -44,8 +46,8 @@ namespace LightlessSync.UI IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(600, 340), - MaximumSize = new(750, 400) + MinimumSize = new(600, 450), + MaximumSize = new(750, 510) }; mediator.Subscribe(this, async _ => await RefreshSyncshells().ConfigureAwait(false)); @@ -137,19 +139,59 @@ namespace LightlessSync.UI { _uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue")); - ImGui.PushTextWrapPos(); - ImGui.Text("This lets other Lightless users know you use Lightless."); - ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); - ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); - ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); - ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); - ImGui.PopTextWrapPos(); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2)); + + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "This lets other Lightless users know you use Lightless."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "While enabled, you and other people using Lightfinder can see each other identified as Lightless users."); + ImGui.Indent(5f); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); + ImGui.PopStyleColor(); + ImGui.Unindent(5f); + + ImGuiHelpers.ScaledDummy(3f); + + _uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue")); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." + + " The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return."); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it.")); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "), + new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way.")); + + ImGuiHelpers.ScaledDummy(3f); + + _uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue")); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Lightfinder is entirely "), + new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server.")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled."); + ImGuiHelpers.ScaledDummy(3f); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("Use it only when you want to be visible."); + ImGui.Text("Use Lightfinder only when you want to be visible."); ImGui.PopStyleColor(); - ImGuiHelpers.ScaledDummy(0.2f); + ImGui.PopStyleVar(); + + ImGuiHelpers.ScaledDummy(2.2f); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) @@ -168,7 +210,7 @@ namespace LightlessSync.UI else { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. + ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. ImGui.PopStyleColor(); } } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 5390c2f..170af20 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -109,6 +109,21 @@ public class CompactUi : WindowMediatorSubscriberBase AllowClickthrough = false; TitleBarButtons = new() { + new TitleBarButton() + { + Icon = FontAwesomeIcon.Cog, + Click = (msg) => + { + Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + }, + IconOffset = new(2,1), + ShowTooltip = () => + { + ImGui.BeginTooltip(); + ImGui.Text("Open Lightless Settings"); + ImGui.EndTooltip(); + } + }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, @@ -431,7 +446,7 @@ public class CompactUi : WindowMediatorSubscriberBase float uidStartX = (contentWidth - uidTextSize.X) / 2f; float cursorY = ImGui.GetCursorPosY(); - if (_configService.Current.BroadcastEnabled) + if (_configService.Current.BroadcastEnabled && _apiController.IsConnected) { float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f; var buttonSize = new Vector2(iconSize.X, uidTextSize.Y); @@ -452,12 +467,6 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.Text("Lightfinder"); ImGui.PopStyleColor(); - ImGui.Text("This lets other Lightless users know you use Lightless."); - ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); - ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); - ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); - ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); ImGui.Text("Use it only when you want to be visible."); ImGui.PopStyleColor(); diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 6c787b3..50c4dbe 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -75,15 +75,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase }); } - void DrawNoteLine(string icon, Vector4 color, string text) - { - _uiSharedService.MediumText(icon, color); - ImGui.SameLine(); - - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); - ImGui.TextWrapped(text); - } - private void LoadVanity() { textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); @@ -101,15 +92,15 @@ public class EditProfileUi : WindowMediatorSubscriberBase _uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow")); ImGui.Dummy(new Vector2(5)); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, 2)); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1)); - DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); - DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); - DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); - DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); ImGui.PopStyleVar(); @@ -286,7 +277,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase { _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(4)); - DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); var hasVanity = _apiController.HasVanity; @@ -332,7 +323,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase const float colorPickAlign = 90f; - DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); + _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleTextColor", ref textEnabled); ImGui.SameLine(); @@ -340,7 +331,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); ImGui.EndDisabled(); - DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); + _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleGlowColor", ref glowEnabled); ImGui.SameLine(); diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 24899ee..eb3acce 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.GameFonts; @@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using System; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -531,6 +532,52 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase { FontText(text, MediumFont, color); } + public void DrawNoteLine(string icon, Vector4 color, string text) + { + MediumText(icon, color); + var iconHeight = ImGui.GetItemRectSize().Y; + + ImGui.SameLine(); + + float textHeight = ImGui.GetTextLineHeight(); + float offset = (iconHeight - textHeight) * 0.5f; + if (offset > 0) + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset); + + ImGui.BeginGroup(); + ImGui.TextWrapped(text); + ImGui.EndGroup(); + } + + public void DrawNoteLine(string icon, Vector4 color, ReadOnlySpan fragments) + { + if (fragments.Length == 0) + { + DrawNoteLine(icon, color, string.Empty); + return; + } + + MediumText(icon, color); + var iconHeight = ImGui.GetItemRectSize().Y; + + ImGui.SameLine(); + + float textHeight = ImGui.GetTextLineHeight(); + float offset = (iconHeight - textHeight) * 0.5f; + if (offset > 0) + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset); + + var wrapWidth = ImGui.GetContentRegionAvail().X; + ImGui.BeginGroup(); + var richText = SeStringUtils.BuildRichText(fragments); + SeStringUtils.RenderSeStringWrapped(richText, wrapWidth); + ImGui.EndGroup(); + } + + public void DrawNoteLine(string icon, Vector4 color, params SeStringUtils.RichTextEntry[] fragments) + { + DrawNoteLine(icon, color, fragments.AsSpan()); + } public bool MediumTreeNode(string label, Vector4? textColor = null, float lineWidth = 2f, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.SpanAvailWidth) { diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index 837d13d..a19a343 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -1,17 +1,23 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Utility; +using Lumina.Text; +using System; using System.Numerics; +using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString; +using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; +using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Utils; public static class SeStringUtils { - public static SeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) + public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) { - var b = new SeStringBuilder(); + var b = new DalamudSeStringBuilder(); if (glowColor is Vector4 glow) b.Add(new GlowPayload(glow)); @@ -30,14 +36,47 @@ public static class SeStringUtils return b.Build(); } - public static SeString BuildPlain(string text) + public static DalamudSeString BuildPlain(string text) { - var b = new SeStringBuilder(); + var b = new DalamudSeStringBuilder(); b.AddText(text ?? string.Empty); return b.Build(); } - public static void RenderSeString(SeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) + public static DalamudSeString BuildRichText(ReadOnlySpan fragments) + { + var builder = new LuminaSeStringBuilder(); + + foreach (var fragment in fragments) + { + if (string.IsNullOrEmpty(fragment.Text)) + continue; + + var hasColor = fragment.Color.HasValue; + Vector4 color = default; + if (hasColor) + { + color = fragment.Color!.Value; + builder.PushColorRgba(color); + } + + if (fragment.Bold) + builder.AppendSetBold(true); + + builder.Append(fragment.Text.AsSpan()); + + if (fragment.Bold) + builder.AppendSetBold(false); + + if (hasColor) + builder.PopColor(); + } + + return DalamudSeString.Parse(builder.ToArray()); + } + + public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan()); + public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) { drawList ??= ImGui.GetWindowDrawList(); @@ -51,9 +90,36 @@ public static class SeStringUtils ImGui.SetCursorScreenPos(position); ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + var textSize = ImGui.CalcTextSize(seString.TextValue); + if (textSize.Y <= 0f) + textSize.Y = ImGui.GetTextLineHeight(); + + ImGui.Dummy(new Vector2(0f, textSize.Y)); } - public static Vector2 RenderSeStringWithHitbox(SeString seString, Vector2 position, ImFontPtr? font = null) + public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null) + { + drawList ??= ImGui.GetWindowDrawList(); + + var drawParams = new SeStringDrawParams + { + Font = font ?? ImGui.GetFont(), + Color = ImGui.GetColorU32(ImGuiCol.Text), + WrapWidth = wrapWidth, + TargetDrawList = drawList + }; + + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f; + var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth); + if (textSize.Y <= 0f) + textSize.Y = ImGui.GetTextLineHeight(); + + ImGui.Dummy(new Vector2(0f, textSize.Y)); + } + public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null) { var drawList = ImGui.GetWindowDrawList(); @@ -99,6 +165,8 @@ public static class SeStringUtils #region Internal Payloads + public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false); + private abstract class AbstractColorPayload : Payload { protected byte Red { get; init; }