using Dalamud.Bindings.ImGui; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using LightlessSync.API.Data; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Services; using LightlessSync.UI.Tags; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Numerics; namespace LightlessSync.UI; public class StandaloneProfileUi : WindowMediatorSubscriberBase { private readonly LightlessProfileManager _lightlessProfileManager; private readonly PairUiService _pairUiService; private readonly ServerConfigurationManager _serverManager; private readonly ProfileTagService _profileTagService; private readonly UiSharedService _uiSharedService; private readonly UserData? _userData; private readonly GroupData? _groupData; private readonly bool _isGroupProfile; private readonly bool _isLightfinderContext; private readonly string? _lightfinderCid; private byte[] _lastProfilePicture = []; private byte[] _lastSupporterPicture = []; private byte[] _lastBannerPicture = []; private IDalamudTextureWrap? _supporterTextureWrap; private IDalamudTextureWrap? _textureWrap; private IDalamudTextureWrap? _bannerTextureWrap; private bool _bannerTextureLoaded; private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); private readonly List _seResolvedSegments = new(); private const float MaxHeightMultiplier = 2.5f; private const float DescriptionMaxVisibleLines = 12f; private const string UserDescriptionPlaceholder = "-- User has no description set --"; private const string GroupDescriptionPlaceholder = "-- Syncshell has no description set --"; private float _lastComputedWindowHeight = -1f; public StandaloneProfileUi( ILogger logger, LightlessMediator mediator, UiSharedService uiBuilder, ServerConfigurationManager serverManager, ProfileTagService profileTagService, LightlessProfileManager lightlessProfileManager, PairUiService pairUiService, Pair? pair, UserData? userData, GroupData? groupData, bool isLightfinderContext, string? lightfinderCid, PerformanceCollectorService performanceCollector) : base(logger, mediator, BuildWindowTitle(userData, groupData, isLightfinderContext), performanceCollector) { _uiSharedService = uiBuilder; _serverManager = serverManager; _profileTagService = profileTagService; _lightlessProfileManager = lightlessProfileManager; Pair = pair; _pairUiService = pairUiService; _userData = userData; _groupData = groupData; _isGroupProfile = groupData is not null; _isLightfinderContext = isLightfinderContext; _lightfinderCid = lightfinderCid; Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; var fixedSize = new Vector2(840f, 525f) * ImGuiHelpers.GlobalScale; Size = fixedSize; SizeCondition = ImGuiCond.Always; SizeConstraints = new() { MinimumSize = fixedSize, MaximumSize = new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier) }; IsOpen = true; } public Pair? Pair { get; } public bool IsGroupProfile => _isGroupProfile; public GroupData? ProfileGroupData => _groupData; public bool IsLightfinderContext => _isLightfinderContext; public string? LightfinderCid => _lightfinderCid; public UserData ProfileUserData => _userData ?? throw new InvalidOperationException("ProfileUserData is only available for user profiles."); public void SetTagColorTheme(Vector4? background, Vector4? border) { if (background.HasValue) _tagBackgroundColor = background.Value; if (border.HasValue) _tagBorderColor = border.Value; } private static Vector4 ResolveThemeColor(string colorName, Vector4 fallback) { try { return UIColors.Get(colorName); } catch (ArgumentException) { // fallback when the color key is not registered } return fallback; } private static string BuildWindowTitle(UserData? userData, GroupData? groupData, bool isLightfinderContext) { if (groupData is not null) { var alias = groupData.AliasOrGID; return $"Syncshell Profile of {alias}##LightlessSyncStandaloneGroupProfileUI{groupData.GID}"; } if (userData is null) return "Lightless Profile##LightlessSyncStandaloneProfileUI"; var name = userData.AliasOrUID; var suffix = isLightfinderContext ? " (Lightfinder)" : string.Empty; return $"Lightless Profile of {name}{suffix}##LightlessSyncStandaloneProfileUI{name}"; } protected override void DrawInternal() { try { if (_isGroupProfile) { DrawGroupProfileWindow(); return; } if (_userData is null) return; var userData = _userData; var scale = ImGuiHelpers.GlobalScale; var viewport = ImGui.GetMainViewport(); var linked = !_isLightfinderContext && Pair is null && ProfileEditorLayoutCoordinator.IsActive(userData.UID); var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale); var baseWidth = baseSize.X; var minHeight = baseSize.Y; var maxAllowedHeight = minHeight * MaxHeightMultiplier; var targetHeight = _lastComputedWindowHeight > 0f ? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight) : minHeight; var desiredSize = new Vector2(baseWidth, targetHeight); Size = desiredSize; if (linked) { ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); var currentPos = ImGui.GetWindowPos(); if (IsWindowBeingDragged()) ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos); var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale); if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); } else { var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale); ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver); ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); } var profile = _lightlessProfileManager.GetLightlessProfile(userData); IReadOnlyList profileTags = profile.Tags.Count > 0 ? ProfileTagService.ResolveTags(profile.Tags) : Array.Empty(); if (_textureWrap == null || !profile.ImageData.Value.SequenceEqual(_lastProfilePicture)) { _textureWrap?.Dispose(); _textureWrap = null; _lastProfilePicture = profile.ImageData.Value; ResetBannerTexture(); if (_lastProfilePicture.Length > 0) { _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); } } if (_supporterTextureWrap == null || !profile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture)) { _supporterTextureWrap?.Dispose(); _supporterTextureWrap = null; if (!string.IsNullOrEmpty(profile.Base64SupporterPicture)) { _lastSupporterPicture = profile.SupporterImageData.Value; if (_lastSupporterPicture.Length > 0) { _supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture); } } } var bannerBytes = profile.BannerImageData.Value; if (!_lastBannerPicture.SequenceEqual(bannerBytes)) { ResetBannerTexture(); _lastBannerPicture = bannerBytes; } string? noteText = null; string statusLabel = _isLightfinderContext ? "Exploring" : "Offline"; string? visiblePlayerName = null; bool directPair = false; bool youPaused = false; bool theyPaused = false; List syncshellLines = new(); if (!_isLightfinderContext && Pair != null) { var snapshot = _pairUiService.GetSnapshot(); noteText = _serverManager.GetNoteForUid(Pair.UserData.UID); statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null; directPair = Pair.IsDirectlyPaired; var pairInfo = Pair.UserPair; if (pairInfo != null) { if (directPair) { youPaused = pairInfo.OwnPermissions.IsPaused(); theyPaused = pairInfo.OtherPermissions.IsPaused(); } if (pairInfo.Groups.Any()) { foreach (var gid in pairInfo.Groups) { var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo) ? groupInfo.GroupAliasOrGID : gid; var groupNote = _serverManager.GetNoteForGid(gid); syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})"); } } } } var presenceTokens = new List { new(statusLabel, string.Equals(statusLabel, "Offline", StringComparison.OrdinalIgnoreCase)) }; if (!string.IsNullOrEmpty(visiblePlayerName)) presenceTokens.Add(new PresenceToken(visiblePlayerName, false)); if (directPair) { presenceTokens.Add(new PresenceToken("Direct Pair", true)); if (youPaused) presenceTokens.Add(new PresenceToken("You paused syncing", true)); if (theyPaused) presenceTokens.Add(new PresenceToken("They paused syncing", true)); } if (syncshellLines.Count > 0) presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", false, syncshellLines, "Shared Syncshells")); var drawList = ImGui.GetWindowDrawList(); var style = ImGui.GetStyle(); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var bannerHeight = 260f * scale; var portraitSize = new Vector2(180f, 180f) * scale; var portraitBorder = 1.5f * scale; var portraitRounding = 1f * scale; var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f); var portraitOverlap = portraitSize.Y * 0.35f; var portraitOffsetX = 35f * scale; var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f; var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap ?? _supporterTextureWrap; string defaultSubtitle = !_isLightfinderContext && Pair != null && !string.IsNullOrEmpty(Pair.UserData.Alias) ? Pair.UserData.Alias! : _isLightfinderContext ? "Lightfinder Session" : noteText ?? string.Empty; bool hasVanityAlias = userData.HasVanity && !string.IsNullOrWhiteSpace(userData.Alias); Vector4? vanityTextColor = null; Vector4? vanityGlowColor = null; if (hasVanityAlias) { if (!string.IsNullOrWhiteSpace(userData.TextColorHex)) vanityTextColor = UIColors.HexToRgba(userData.TextColorHex); if (!string.IsNullOrWhiteSpace(userData.TextGlowColorHex)) vanityGlowColor = UIColors.HexToRgba(userData.TextGlowColorHex); } bool useVanityColors = vanityTextColor.HasValue || vanityGlowColor.HasValue; string primaryHeaderText = hasVanityAlias ? userData.Alias! : userData.UID; List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = new(); if (hasVanityAlias) { secondaryHeaderLines.Add((userData.UID, useVanityColors, false)); if (!string.IsNullOrEmpty(defaultSubtitle) && !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase) && !string.Equals(defaultSubtitle, userData.Alias, StringComparison.OrdinalIgnoreCase)) { secondaryHeaderLines.Add((defaultSubtitle, false, true)); } } else if (!string.IsNullOrEmpty(defaultSubtitle) && !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase)) { secondaryHeaderLines.Add((defaultSubtitle, false, true)); } var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); var bannerMin = windowPos - bannerScrollOffset; var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight); if (bannerTexture != null) { drawList.AddImage( bannerTexture.Handle, bannerMin, bannerMax); } else { var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header)); var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f)); topColor.W = 1f; var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f)); bottomColor.W = 1f; drawList.AddRectFilledMultiColor( bannerMin, bannerMax, ImGui.ColorConvertFloat4ToU32(topColor), ImGui.ColorConvertFloat4ToU32(topColor), ImGui.ColorConvertFloat4ToU32(bottomColor), ImGui.ColorConvertFloat4ToU32(bottomColor)); } if (_supporterTextureWrap != null) { const float iconBaseSize = 40f; var iconPadding = new Vector2(style.WindowPadding.X + 18f * scale, style.WindowPadding.Y + 18f * scale); var textureWidth = MathF.Max(1f, _supporterTextureWrap.Width); var textureHeight = MathF.Max(1f, _supporterTextureWrap.Height); var textureMaxEdge = MathF.Max(textureWidth, textureHeight); var iconScale = (iconBaseSize * scale) / textureMaxEdge; var iconSize = new Vector2(textureWidth * iconScale, textureHeight * iconScale); var iconMax = bannerMax - iconPadding; var iconMin = iconMax - iconSize; var backgroundPadding = 6f * scale; var iconBackgroundMin = iconMin - new Vector2(backgroundPadding); var iconBackgroundMax = iconMax + new Vector2(backgroundPadding); var backgroundColor = new Vector4(0f, 0f, 0f, 0.65f); var cornerRadius = MathF.Max(4f * scale, iconSize.Y * 0.25f); drawList.AddRectFilled(iconBackgroundMin, iconBackgroundMax, ImGui.GetColorU32(backgroundColor), cornerRadius); drawList.AddImage(_supporterTextureWrap.Handle, iconMin, iconMax); } var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap); var topAreaStart = ImGui.GetCursorPos(); var portraitBackgroundPadding = 12f * scale; var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f); var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale); ImGui.SetCursorPos(portraitAreaPos); var portraitAreaScreenPos = ImGui.GetCursorScreenPos(); ImGui.Dummy(portraitAreaSize); var portraitAreaMin = portraitAreaScreenPos; var portraitAreaMax = portraitAreaMin + portraitAreaSize; var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding); var portraitFrameMax = portraitFrameMin + portraitFrameSize; var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg]; portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f); drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding); var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border]; if (_textureWrap != null) { drawList.AddImageRounded( _textureWrap.Handle, portraitFrameMin + new Vector2(portraitBorder, portraitBorder), portraitFrameMax - new Vector2(portraitBorder, portraitBorder), Vector2.Zero, Vector2.One, 0xFFFFFFFF, portraitRounding); } else { drawList.AddRect( portraitFrameMin + new Vector2(portraitBorder, portraitBorder), portraitFrameMax - new Vector2(portraitBorder, portraitBorder), ImGui.GetColorU32(portraitFrameBorder), portraitRounding); } var portraitAreaLocalMin = portraitAreaMin - windowPos; var portraitAreaLocalMax = portraitAreaMax - windowPos; var portraitFrameLocalMin = portraitFrameMin - windowPos; var portraitFrameLocalMax = portraitFrameMax - windowPos; var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y; var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y); var aliasColumnX = infoOffsetX + 18f * scale; ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY)); ImGui.BeginGroup(); using (_uiSharedService.UidFont.Push()) { if (useVanityColors) { var seString = SeStringUtils.BuildFormattedPlayerName(primaryHeaderText, vanityTextColor, vanityGlowColor); SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); } else { ImGui.TextUnformatted(primaryHeaderText); } } foreach (var (text, useColor, disabled) in secondaryHeaderLines) { if (useColor && useVanityColors) { var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor); SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); } else { if (disabled) ImGui.TextDisabled(text); else ImGui.TextUnformatted(text); } } ImGui.EndGroup(); var namesEnd = ImGui.GetCursorPos(); var namesBlockBottom = windowPos.Y + namesEnd.Y; var aliasGroupRectMin = ImGui.GetItemRectMin(); var aliasGroupRectMax = ImGui.GetItemRectMax(); var aliasGroupLocalMin = aliasGroupRectMin - windowPos; var aliasGroupLocalMax = aliasGroupRectMax - windowPos; var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale); ImGui.SetCursorPos(tagsStartLocal); RenderProfileTags(profileTags, scale); var tagsEndLocal = ImGui.GetCursorPos(); var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y; var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y; var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y); var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom); var descriptionPreSpacing = style.ItemSpacing.Y * 1.35f; var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionPreSpacing); var horizontalInset = style.ItemSpacing.X * 0.5f; var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.5f; var descriptionSeparatorThickness = MathF.Max(1f, scale); var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y); var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y); drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness); var descriptionContentStartLocal = descriptionStartLocal + new Vector2(0f, descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f); ImGui.SetCursorPos(descriptionContentStartLocal); ImGui.TextDisabled("Description"); ImGui.SetCursorPosX(aliasColumnX); var descriptionRegionWidth = ImGui.GetContentRegionAvail().X; if (descriptionRegionWidth <= 0f) descriptionRegionWidth = 1f; var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f); var hasDescription = !string.IsNullOrWhiteSpace(profile.Description); float descriptionContentHeight; float lineHeightWithSpacing; using (_uiSharedService.GameFont.Push()) { lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); var measurementText = hasDescription ? NormalizeDescriptionForMeasurement(profile.Description!) : UserDescriptionPlaceholder; if (string.IsNullOrWhiteSpace(measurementText)) measurementText = UserDescriptionPlaceholder; descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y; if (descriptionContentHeight <= 0f) descriptionContentHeight = lineHeightWithSpacing; } var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines; var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight); RenderDescriptionChild( "##StandaloneProfileDescription", new Vector2(descriptionRegionWidth, descriptionChildHeight), hasDescription ? profile.Description : null, UserDescriptionPlaceholder); var descriptionEndLocal = ImGui.GetCursorPos(); var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y; aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y); aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom); var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f; var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y); var presenceStartLocal = new Vector2( portraitFrameLocalMin.X, presenceAnchorY + presenceLabelSpacing); ImGui.SetCursorPos(presenceStartLocal); ImGui.TextDisabled("Presence"); ImGui.SetCursorPosX(portraitFrameLocalMin.X); if (presenceTokens.Count > 0) { var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X); RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth); } else { ImGui.SetCursorPosX(portraitFrameLocalMin.X); ImGui.TextDisabled("-- No presence information --"); ImGui.SetCursorPosX(portraitFrameLocalMin.X); ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f)); } var presenceContentEnd = ImGui.GetCursorPos(); var separatorSpacing = style.ItemSpacing.Y * 0.2f; var separatorThickness = MathF.Max(1f, scale); var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing); var separatorStart = windowPos + separatorStartLocal; var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y); drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness); var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f); var columnStartLocalY = afterSeparatorLocal.Y; var leftColumnX = portraitFrameLocalMin.X; var leftWrapPos = windowPos.X + aliasColumnX - style.ItemSpacing.X; ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY)); float leftColumnEndY = columnStartLocalY; if (!string.IsNullOrEmpty(noteText)) { ImGui.TextDisabled("Notes"); ImGui.SetCursorPosX(leftColumnX); ImGui.PushTextWrapPos(leftWrapPos); ImGui.TextUnformatted(noteText); ImGui.PopTextWrapPos(); ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f)); leftColumnEndY = ImGui.GetCursorPosY(); } leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY()); var columnsBottomLocal = leftColumnEndY; var columnsBottom = windowPos.Y + columnsBottomLocal; var topAreaBase = windowPos.Y + topAreaStart.Y; var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom); var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom); var topAreaHeight = leftBlockBottom - topAreaBase; if (topAreaHeight < 0f) topAreaHeight = 0f; ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y)); var finalCursorY = ImGui.GetCursorPosY(); var paddingY = ImGui.GetStyle().WindowPadding.Y; var computedHeight = finalCursorY + paddingY; var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight); _lastComputedWindowHeight = adjustedHeight; var finalSize = new Vector2(baseWidth, adjustedHeight); Size = finalSize; ImGui.SetWindowSize(finalSize, ImGuiCond.Always); } catch (Exception ex) { _logger.LogWarning(ex, "Error during standalone profile draw"); } } private IDalamudTextureWrap? GetIconWrap(uint iconId) { try { if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) return wrap; } catch (Exception ex) { _logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId); } return null; } private void RenderDescriptionChild( string childId, Vector2 childSize, string? description, string placeholderText) { ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f); if (ImGui.BeginChild(childId, childSize, false)) { using (_uiSharedService.GameFont.Push()) { ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); if (string.IsNullOrWhiteSpace(description)) { ImGui.TextUnformatted(placeholderText); } else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(description)) { ImGui.TextUnformatted(description); } ImGui.PopTextWrapPos(); } } ImGui.EndChild(); ImGui.PopStyleVar(); } private void RenderProfileTags(IReadOnlyList tags, float scale) { if (tags.Count == 0) { ImGui.TextDisabled("-- No tags set --"); return; } var drawList = ImGui.GetWindowDrawList(); var style = ImGui.GetStyle(); var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); var startLocal = ImGui.GetCursorPos(); var startScreen = ImGui.GetCursorScreenPos(); float availableWidth = ImGui.GetContentRegionAvail().X; if (availableWidth <= 0f) availableWidth = 1f; float cursorX = startScreen.X; float cursorY = startScreen.Y; float rowHeight = 0f; for (int i = 0; i < tags.Count; i++) { var tag = tags[i]; if (!tag.HasContent) continue; var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); var tagWidth = tagSize.X; var tagHeight = tagSize.Y; if (cursorX > startScreen.X && cursorX + tagWidth > startScreen.X + availableWidth) { cursorX = startScreen.X; cursorY += rowHeight + style.ItemSpacing.Y; rowHeight = 0f; } var tagPos = new Vector2(cursorX, cursorY); ImGui.SetCursorScreenPos(tagPos); ImGui.InvisibleButton($"##profileTag_{i}", tagSize); ProfileTagRenderer.RenderTag(tag, tagPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); cursorX += tagWidth + style.ItemSpacing.X; rowHeight = MathF.Max(rowHeight, tagHeight); } var totalHeight = (cursorY + rowHeight) - startScreen.Y; if (totalHeight < 0f) totalHeight = 0f; ImGui.SetCursorPos(new Vector2(startLocal.X, startLocal.Y + totalHeight)); } private void DrawGroupProfileWindow() { if (_groupData is null) return; var scale = ImGuiHelpers.GlobalScale; var viewport = ImGui.GetMainViewport(); var linked = ProfileEditorLayoutCoordinator.IsActive(_groupData.GID); var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale); var baseWidth = baseSize.X; var minHeight = baseSize.Y; var maxAllowedHeight = minHeight * MaxHeightMultiplier; var targetHeight = _lastComputedWindowHeight > 0f ? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight) : minHeight; var desiredSize = new Vector2(baseWidth, targetHeight); Size = desiredSize; if (linked) { ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); var currentPos = ImGui.GetWindowPos(); if (IsWindowBeingDragged()) ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos); var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale); if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); } else { var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale); ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver); ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); } var profile = _lightlessProfileManager.GetLightlessGroupProfile(_groupData); IReadOnlyList profileTags = profile.Tags.Count > 0 ? ProfileTagService.ResolveTags(profile.Tags) : Array.Empty(); if (_textureWrap == null || !profile.ProfileImageData.Value.SequenceEqual(_lastProfilePicture)) { _textureWrap?.Dispose(); _textureWrap = null; _lastProfilePicture = profile.ProfileImageData.Value; ResetBannerTexture(); if (_lastProfilePicture.Length > 0) { _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); } } if (_supporterTextureWrap != null) { _supporterTextureWrap.Dispose(); _supporterTextureWrap = null; } _lastSupporterPicture = Array.Empty(); var bannerBytes = profile.BannerImageData.Value; if (!_lastBannerPicture.SequenceEqual(bannerBytes)) { ResetBannerTexture(); _lastBannerPicture = bannerBytes; } var noteText = _serverManager.GetNoteForGid(_groupData.GID); var presenceTokens = new List { new(profile.IsDisabled ? "Disabled" : "Active", profile.IsDisabled) }; if (profile.IsNsfw) presenceTokens.Add(new PresenceToken("NSFW", true)); int memberCount = 0; List? groupMembers = null; var snapshot = _pairUiService.GetSnapshot(); GroupFullInfoDto groupInfo = null; if (_groupData is not null && snapshot.GroupsByGid.TryGetValue(_groupData.GID, out var refreshedGroupInfo)) { groupInfo = refreshedGroupInfo; } if (groupInfo is not null && snapshot.GroupPairs.TryGetValue(groupInfo, out var pairsForGroup)) { groupMembers = pairsForGroup.ToList(); memberCount = groupMembers.Count; } else if (groupInfo?.GroupPairUserInfos is { Count: > 0 }) { memberCount = groupInfo.GroupPairUserInfos.Count; } string memberLabel = memberCount == 1 ? "1 Member" : $"{memberCount} Members"; presenceTokens.Add(new PresenceToken(memberLabel, false)); if (groupInfo?.GroupPermissions.IsDisableInvites() ?? false) { presenceTokens.Add(new PresenceToken( "Invites Locked", true, new[] { "New members cannot join while this lock is active." }, "Syncshell Status")); } var drawList = ImGui.GetWindowDrawList(); var style = ImGui.GetStyle(); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var bannerHeight = 260f * scale; var portraitSize = new Vector2(180f, 180f) * scale; var portraitBorder = 1.5f * scale; var portraitRounding = 1f * scale; var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f); var portraitOverlap = portraitSize.Y * 0.35f; var portraitOffsetX = 35f * scale; var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f; var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap; var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); var bannerMin = windowPos - bannerScrollOffset; var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight); if (bannerTexture != null) { drawList.AddImage( bannerTexture.Handle, bannerMin, bannerMax); } else { var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header)); var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f)); topColor.W = 1f; var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f)); bottomColor.W = 1f; drawList.AddRectFilledMultiColor( bannerMin, bannerMax, ImGui.ColorConvertFloat4ToU32(topColor), ImGui.ColorConvertFloat4ToU32(topColor), ImGui.ColorConvertFloat4ToU32(bottomColor), ImGui.ColorConvertFloat4ToU32(bottomColor)); } var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap); var topAreaStart = ImGui.GetCursorPos(); var portraitBackgroundPadding = 12f * scale; var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f); var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale); ImGui.SetCursorPos(portraitAreaPos); var portraitAreaScreenPos = ImGui.GetCursorScreenPos(); ImGui.Dummy(portraitAreaSize); var contentStart = ImGui.GetCursorPos(); var portraitAreaMin = portraitAreaScreenPos; var portraitAreaMax = portraitAreaMin + portraitAreaSize; var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding); var portraitFrameMax = portraitFrameMin + portraitFrameSize; var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg]; portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f); drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding); var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border]; if (_textureWrap != null) { drawList.AddImageRounded( _textureWrap.Handle, portraitFrameMin + new Vector2(portraitBorder, portraitBorder), portraitFrameMax - new Vector2(portraitBorder, portraitBorder), Vector2.Zero, Vector2.One, 0xFFFFFFFF, portraitRounding); } else { drawList.AddRect( portraitFrameMin + new Vector2(portraitBorder, portraitBorder), portraitFrameMax - new Vector2(portraitBorder, portraitBorder), ImGui.GetColorU32(portraitFrameBorder), portraitRounding); } drawList.AddRect(portraitFrameMin, portraitFrameMax, ImGui.GetColorU32(portraitFrameBorder), portraitRounding); var portraitAreaLocalMax = portraitAreaMax - windowPos; var portraitFrameLocalMin = portraitFrameMin - windowPos; var portraitFrameLocalMax = portraitFrameMax - windowPos; var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y; ImGui.SetCursorPos(contentStart); bool useVanityColors = false; Vector4? vanityTextColor = null; Vector4? vanityGlowColor = null; if (_groupData is not null && groupInfo is not null) { string primaryHeaderText = _groupData.AliasOrGID; List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = [ (_groupData.GID, false, true) ]; if (groupInfo.Owner is not null) secondaryHeaderLines.Add(($"Owner: {groupInfo.Owner.AliasOrUID}", false, true)); var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y); var aliasColumnX = infoOffsetX + 18f * scale; ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY)); ImGui.BeginGroup(); using (_uiSharedService.UidFont.Push()) { ImGui.TextUnformatted(primaryHeaderText); } foreach (var (text, useColor, disabled) in secondaryHeaderLines) { if (useColor && useVanityColors) { var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor); SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); } else { if (disabled) ImGui.TextDisabled(text); else ImGui.TextUnformatted(text); } } ImGui.EndGroup(); var namesEnd = ImGui.GetCursorPos(); var aliasGroupRectMin = ImGui.GetItemRectMin(); var aliasGroupRectMax = ImGui.GetItemRectMax(); var aliasGroupLocalMin = aliasGroupRectMin - windowPos; var aliasGroupLocalMax = aliasGroupRectMax - windowPos; var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale); ImGui.SetCursorPos(tagsStartLocal); if (profileTags.Count > 0) RenderProfileTags(profileTags, scale); else ImGui.TextDisabled("-- No tags set --"); var tagsEndLocal = ImGui.GetCursorPos(); var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y; var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y; var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y); var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom); var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.35f; var descriptionSeparatorThickness = MathF.Max(1f, scale); var descriptionExtraOffset = groupInfo.Owner is not null ? style.ItemSpacing.Y * 0.6f : 0f; var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionSeparatorSpacing + descriptionExtraOffset); var horizontalInset = style.ItemSpacing.X * 0.5f; var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y); var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y); drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness); var descriptionContentStartLocal = new Vector2(aliasColumnX, descriptionStartLocal.Y + descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f); ImGui.SetCursorPos(descriptionContentStartLocal); ImGui.TextDisabled("Description"); ImGui.SetCursorPosX(aliasColumnX); var descriptionRegionWidth = ImGui.GetContentRegionAvail().X; if (descriptionRegionWidth <= 0f) descriptionRegionWidth = 1f; var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f); var hasDescription = !string.IsNullOrWhiteSpace(profile.Description); float descriptionContentHeight; float lineHeightWithSpacing; using (_uiSharedService.GameFont.Push()) { lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); var measurementText = hasDescription ? NormalizeDescriptionForMeasurement(profile.Description!) : GroupDescriptionPlaceholder; if (string.IsNullOrWhiteSpace(measurementText)) measurementText = GroupDescriptionPlaceholder; descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y; if (descriptionContentHeight <= 0f) descriptionContentHeight = lineHeightWithSpacing; } var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines; var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight); RenderDescriptionChild( "##StandaloneGroupDescription", new Vector2(descriptionRegionWidth, descriptionChildHeight), hasDescription ? profile.Description : null, GroupDescriptionPlaceholder); var descriptionEndLocal = ImGui.GetCursorPos(); var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y; aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y); aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom); var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f; var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y); var presenceStartLocal = new Vector2(portraitFrameLocalMin.X, presenceAnchorY + presenceLabelSpacing); ImGui.SetCursorPos(presenceStartLocal); ImGui.TextDisabled("Presence"); ImGui.SetCursorPosX(portraitFrameLocalMin.X); if (presenceTokens.Count > 0) { var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X); RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth); } else { ImGui.TextDisabled("-- No status flags --"); ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f)); } var presenceContentEnd = ImGui.GetCursorPos(); var separatorSpacing = style.ItemSpacing.Y * 0.2f; var separatorThickness = MathF.Max(1f, scale); var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing); var separatorStart = windowPos + separatorStartLocal; var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y); drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness); var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f); var columnStartLocalY = afterSeparatorLocal.Y; var leftColumnX = portraitFrameLocalMin.X; ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY)); float leftColumnEndY = columnStartLocalY; if (!string.IsNullOrEmpty(noteText)) { ImGui.TextDisabled("Notes"); ImGui.SetCursorPosX(leftColumnX); ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); ImGui.TextUnformatted(noteText); ImGui.PopTextWrapPos(); ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f)); leftColumnEndY = ImGui.GetCursorPosY(); } leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY()); var columnsBottomLocal = leftColumnEndY; var columnsBottom = windowPos.Y + columnsBottomLocal; var topAreaBase = windowPos.Y + topAreaStart.Y; var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom); var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom); var topAreaHeight = leftBlockBottom - topAreaBase; if (topAreaHeight < 0f) topAreaHeight = 0f; ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y)); var finalCursorY = ImGui.GetCursorPosY(); var paddingY = ImGui.GetStyle().WindowPadding.Y; var computedHeight = finalCursorY + paddingY; var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight); _lastComputedWindowHeight = adjustedHeight; var finalSize = new Vector2(baseWidth, adjustedHeight); Size = finalSize; ImGui.SetWindowSize(finalSize, ImGuiCond.Always); } } private IDalamudTextureWrap? GetBannerTexture(byte[] bannerBytes) { if (_bannerTextureLoaded) return _bannerTextureWrap; _bannerTextureLoaded = true; if (bannerBytes.Length == 0) return null; _bannerTextureWrap = _uiSharedService.LoadImage(bannerBytes); return _bannerTextureWrap; } private void ResetBannerTexture() { _bannerTextureWrap?.Dispose(); _bannerTextureWrap = null; _lastBannerPicture = []; _bannerTextureLoaded = false; } private static bool IsWindowBeingDragged() { return ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && ImGui.GetIO().MouseDown[0]; } private static string NormalizeDescriptionForMeasurement(string description) { if (string.IsNullOrWhiteSpace(description)) return string.Empty; var normalized = description.ReplaceLineEndings("\n"); normalized = normalized .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) .Replace("
", "\n", StringComparison.OrdinalIgnoreCase); return SeStringUtils.StripMarkup(normalized); } private static void RenderPresenceTokens(IReadOnlyList tokens, float scale, float? maxWidth = null) { if (tokens.Count == 0) return; var drawList = ImGui.GetWindowDrawList(); var style = ImGui.GetStyle(); var startPos = ImGui.GetCursorPos(); float startX = startPos.X; float cursorX = startX; float cursorY = startPos.Y; float availWidth = maxWidth ?? ImGui.GetContentRegionAvail().X; if (availWidth <= 0f) availWidth = ImGui.GetContentRegionAvail().X; if (availWidth <= 0f) availWidth = 1f; float spacingX = style.ItemSpacing.X; float spacingY = style.ItemSpacing.Y; float rounding = style.FrameRounding > 0f ? style.FrameRounding : 6f * scale; var padding = new Vector2(8f * scale, 4f * scale); var baseColor = new Vector4(0.16f, 0.16f, 0.16f, 0.95f); var baseBorder = style.Colors[(int)ImGuiCol.Border]; baseBorder.W *= 0.35f; var alertColor = new Vector4(0.32f, 0.2f, 0.2f, 0.95f); var alertBorder = Vector4.Lerp(baseBorder, new Vector4(0.9f, 0.5f, 0.5f, baseBorder.W), 0.6f); var textColor = style.Colors[(int)ImGuiCol.Text]; float rowHeight = 0f; for (int i = 0; i < tokens.Count; i++) { var token = tokens[i]; var textSize = ImGui.CalcTextSize(token.Text); var tagSize = textSize + padding * 2f; if (cursorX > startX && cursorX + tagSize.X > startX + availWidth) { cursorX = startX; cursorY += rowHeight + spacingY; rowHeight = 0f; } ImGui.SetCursorPos(new Vector2(cursorX, cursorY)); ImGui.InvisibleButton($"##presenceTag_{i}", tagSize); var tagMin = ImGui.GetItemRectMin(); var tagMax = ImGui.GetItemRectMax(); var fillColor = token.Emphasis ? alertColor : baseColor; var borderColor = token.Emphasis ? alertBorder : baseBorder; drawList.AddRectFilled(tagMin, tagMax, ImGui.GetColorU32(fillColor), rounding); drawList.AddRect(tagMin, tagMax, ImGui.GetColorU32(borderColor), rounding); drawList.AddText(tagMin + padding, ImGui.GetColorU32(textColor), token.Text); if (token.Tooltip is { Count: > 0 }) { if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); if (!string.IsNullOrEmpty(token.TooltipTitle)) { ImGui.TextUnformatted(token.TooltipTitle); ImGui.Separator(); } foreach (var line in token.Tooltip) ImGui.TextUnformatted(line); ImGui.EndTooltip(); } } cursorX += tagSize.X + spacingX; rowHeight = MathF.Max(rowHeight, tagSize.Y); } ImGui.SetCursorPos(new Vector2(startX, cursorY + rowHeight)); ImGui.Dummy(new Vector2(0f, spacingY * 0.25f)); } public override void OnClose() { if (!_isGroupProfile && !_isLightfinderContext && Pair is null && _userData is not null && ProfileEditorLayoutCoordinator.IsActive(_userData.UID)) { ProfileEditorLayoutCoordinator.Disable(_userData.UID); } else if (_isGroupProfile && _groupData is not null && ProfileEditorLayoutCoordinator.IsActive(_groupData.GID)) { ProfileEditorLayoutCoordinator.Disable(_groupData.GID); } Mediator.Publish(new RemoveWindowMessage(this)); } private readonly record struct PresenceToken( string Text, bool Emphasis, IReadOnlyList? Tooltip = null, string? TooltipTitle = null); }