Files
LightlessClient/LightlessSync/UI/StandaloneProfileUi.cs
2025-12-28 16:28:27 +01:00

1290 lines
56 KiB
C#

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<SeStringUtils.SeStringSegment> _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<StandaloneProfileUi> 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<ProfileTagDefinition> 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<string> 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<PresenceToken>
{
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<ProfileTagDefinition> 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<ProfileTagDefinition> profileTags = profile.Tags.Count > 0
? ProfileTagService.ResolveTags(profile.Tags)
: Array.Empty<ProfileTagDefinition>();
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<byte>();
var bannerBytes = profile.BannerImageData.Value;
if (!_lastBannerPicture.SequenceEqual(bannerBytes))
{
ResetBannerTexture();
_lastBannerPicture = bannerBytes;
}
var noteText = _serverManager.GetNoteForGid(_groupData.GID);
var presenceTokens = new List<PresenceToken>
{
new(profile.IsDisabled ? "Disabled" : "Active", profile.IsDisabled)
};
if (profile.IsNsfw)
presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true));
int memberCount = 0;
List<Pair>? 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("<br>", "\n", StringComparison.OrdinalIgnoreCase)
.Replace("<br/>", "\n", StringComparison.OrdinalIgnoreCase)
.Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase)
.Replace("</br>", "\n", StringComparison.OrdinalIgnoreCase);
return SeStringUtils.StripMarkup(normalized);
}
private static void RenderPresenceTokens(IReadOnlyList<PresenceToken> 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<string>? Tooltip = null,
string? TooltipTitle = null);
}