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 LightlessSync.WebAPI; 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 ApiController _apiController; 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 const string LightfinderDisplayName = "Lightfinder User"; private readonly string _lightfinderDisplayName = LightfinderDisplayName; private float _lastComputedWindowHeight = -1f; public StandaloneProfileUi( ILogger logger, LightlessMediator mediator, UiSharedService uiBuilder, ServerConfigurationManager serverManager, ProfileTagService profileTagService, DalamudUtilService dalamudUtilService, LightlessProfileManager lightlessProfileManager, PairUiService pairUiService, Pair? pair, UserData? userData, GroupData? groupData, bool isLightfinderContext, string? lightfinderCid, PerformanceCollectorService performanceCollector, ApiController apiController) : base(logger, mediator, BuildWindowTitle( userData, groupData, isLightfinderContext, isLightfinderContext ? ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid) : null), 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; if (_isLightfinderContext) _lightfinderDisplayName = ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid); Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; var fixedSize = new Vector2(840f, 525f) * ImGuiHelpers.GlobalScale; Size = fixedSize; SizeCondition = ImGuiCond.Always; WindowBuilder.For(this) .SetSizeConstraints( fixedSize, new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier)) .Apply(); IsOpen = true; _apiController = apiController; } 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, string? lightfinderDisplayName) { 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 = isLightfinderContext ? lightfinderDisplayName ?? LightfinderDisplayName : userData.AliasOrUID; var suffix = isLightfinderContext ? " (Lightfinder)" : string.Empty; return $"Lightless Profile of {name}{suffix}##LightlessSyncStandaloneProfileUI{name}"; } private static string ResolveLightfinderDisplayName(DalamudUtilService dalamudUtilService, string? hashedCid) { if (string.IsNullOrEmpty(hashedCid)) return LightfinderDisplayName; try { var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); if (string.IsNullOrEmpty(name)) return LightfinderDisplayName; var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; } catch { return LightfinderDisplayName; } } 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) : []; 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; var isSelfProfile = !_isLightfinderContext && _userData is not null && !string.IsNullOrEmpty(_apiController.UID) && string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal); string statusLabel = _isLightfinderContext ? "Exploring" : isSelfProfile ? "Online" : "Offline"; string? visiblePlayerName = null; bool directPair = false; bool youPaused = false; bool theyPaused = false; List syncshellLines = []; if (!_isLightfinderContext) { noteText = _serverManager.GetNoteForUid(_userData!.UID); } 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.Count != 0) { 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})"); } } } if (isSelfProfile) statusLabel = "Online"; } 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 (profile.IsNSFW) presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true)); if (syncshellLines.Count > 0) presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", Emphasis: 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 = !_isLightfinderContext && 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 = _isLightfinderContext ? _lightfinderDisplayName : hasVanityAlias ? userData.Alias! : userData.UID; List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = new(); if (!_isLightfinderContext && 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", Emphasis: 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; string primaryHeaderText = _groupData.AliasOrGID; List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = [ (_groupData.GID, false, true) ]; if (groupInfo is not null) secondaryHeaderLines.Add(($"Owner: {groupInfo.Owner.AliasOrUID}", false, true)); else secondaryHeaderLines.Add(($"Unknown Owner", 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 = 0f; if (groupInfo?.Owner is not null) descriptionExtraOffset = style.ItemSpacing.Y * 0.6f; 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); }