Files
LightlessClient/LightlessSync/UI/Handlers/IdDisplayHandler.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>)
 - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
2026-01-20 19:43:00 +00:00

533 lines
19 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
namespace LightlessSync.UI.Handlers;
public class IdDisplayHandler
{
private readonly LightlessConfigService _lightlessConfigService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LightlessMediator _mediator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, bool> _showIdForEntry = new(StringComparer.Ordinal);
private string _editComment = string.Empty;
private string _editEntry = string.Empty;
private bool _editIsUid = false;
private string _lastMouseOverUid = string.Empty;
private bool _popupShown = false;
private DateTime? _popupTime;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private float _highlightBoost;
public IdDisplayHandler(
LightlessMediator mediator,
ServerConfigurationManager serverManager,
LightlessConfigService lightlessConfigService,
PlayerPerformanceConfigService playerPerformanceConfigService)
{
_mediator = mediator;
_serverManager = serverManager;
_lightlessConfigService = lightlessConfigService;
_playerPerformanceConfigService = playerPerformanceConfigService;
}
public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, Func<float> editBoxWidth)
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetGroupText(group);
if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal))
{
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid))
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(playerText);
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Left click to switch between ID display and alias"
+ Environment.NewLine + "Right click to edit notes for this syncshell"
+ Environment.NewLine + "Middle Mouse Button to open syncshell profile in a separate window");
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsUid;
if (_showIdForEntry.TryGetValue(group.GID, out bool value))
{
prevState = value;
}
_showIdForEntry[group.GID] = !prevState;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (_editIsUid)
{
_serverManager.SetNoteForUid(_editEntry, _editComment, save: true);
}
else
{
_serverManager.SetNoteForGid(_editEntry, _editComment, save: true);
}
_editComment = _serverManager.GetNoteForGid(group.GID) ?? string.Empty;
_editEntry = group.GID;
_editIsUid = false;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Middle))
{
_mediator.Publish(new GroupProfileOpenStandaloneMessage(group.Group));
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.SetNextItemWidth(editBoxWidth.Invoke());
if (ImGui.InputTextWithHint("", "Name/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForGid(group.GID, _editComment, save: true);
_editEntry = string.Empty;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_editEntry = string.Empty;
}
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
}
public void DrawPairText(string id, Pair pair, float textPosX, Func<float> editBoxWidth)
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetPlayerText(pair);
var compactPerformanceText = BuildCompactPerformanceUsageText(pair);
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
{
var targetFontSize = ImGui.GetFontSize();
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f);
float rowRightLimit = 0f;
Vector2 nameRectMin = Vector2.Zero;
Vector2 nameRectMax = Vector2.Zero;
float rowTopForStats = 0f;
float frameHeightForStats = 0f;
using (ImRaii.PushFont(font, textIsUid))
{
ImGui.AlignTextToFramePadding();
var rowStart = ImGui.GetCursorScreenPos();
rowRightLimit = rowStart.X + rowWidth;
Vector4? textColor = null;
Vector4? glowColor = null;
if (pair.UserData.HasVanity)
{
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
{
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
{
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
}
}
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
var seString = useVanityColors
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
: SeStringUtils.BuildPlain(playerText);
var drawList = ImGui.GetWindowDrawList();
bool useHighlight = false;
float highlightPadX = 0f;
float highlightPadY = 0f;
if (useVanityColors)
{
float boost = Luminance.ComputeHighlight(textColor, glowColor);
if (boost > 0f)
{
var style = ImGui.GetStyle();
useHighlight = true;
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
_highlightBoost = boost;
}
else
{
_highlightBoost = 0f;
}
}
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, targetFontSize, font, pair.UserData.UID);
nameRectMin = ImGui.GetItemRectMin();
nameRectMax = ImGui.GetItemRectMax();
if (useHighlight)
{
var style = ImGui.GetStyle();
var frameHeight = ImGui.GetFrameHeight();
var rowTop = rowStart.Y - style.FramePadding.Y;
var rowBottom = rowTop + frameHeight;
var highlightMin = new Vector2(nameRectMin.X - highlightPadX, rowTop - highlightPadY);
var highlightMax = new Vector2(nameRectMax.X + highlightPadX, rowBottom + highlightPadY);
var windowPos = ImGui.GetWindowPos();
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
var highlightColor = new Vector4(
0.25f + _highlightBoost,
0.25f + _highlightBoost,
0.25f + _highlightBoost,
1f
);
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
drawList.ChannelsSetCurrent(0);
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
var borderColor = style.Colors[(int)ImGuiCol.Border];
borderColor.W *= 0.25f;
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
drawList.ChannelsMerge();
}
}
{
var style = ImGui.GetStyle();
frameHeightForStats = ImGui.GetFrameHeight();
rowTopForStats = nameRectMin.Y - style.FramePadding.Y;
}
if (ImGui.IsItemHovered())
{
if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{
_popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay);
}
_lastMouseOverUid = id;
if (_popupTime > DateTime.UtcNow || !_lightlessConfigService.Current.ProfilesShow)
{
ImGui.SetTooltip("Left click to switch between UID display and nick" + Environment.NewLine
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
+ "Middle Mouse Button to open their profile in a separate window");
}
else if (_popupTime < DateTime.UtcNow && !_popupShown)
{
_popupShown = true;
_mediator.Publish(new ProfilePopoutToggle(pair));
}
}
else
{
if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{
_mediator.Publish(new ProfilePopoutToggle(Pair: null));
_lastMouseOverUid = string.Empty;
_popupShown = false;
}
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsUid;
if (_showIdForEntry.ContainsKey(pair.UserData.UID))
{
prevState = _showIdForEntry[pair.UserData.UID];
}
_showIdForEntry[pair.UserData.UID] = !prevState;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (_editIsUid)
{
_serverManager.SetNoteForUid(_editEntry, _editComment, save: true);
}
else
{
_serverManager.SetNoteForGid(_editEntry, _editComment, save: true);
}
_editComment = pair.GetNote() ?? string.Empty;
_editEntry = pair.UserData.UID;
_editIsUid = true;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Middle))
{
_mediator.Publish(new ProfileOpenStandaloneMessage(pair));
}
if (!string.IsNullOrEmpty(compactPerformanceText))
{
ImGui.SameLine();
const float compactFontScale = 0.85f;
ImGui.SetWindowFontScale(compactFontScale);
var compactHeight = ImGui.GetTextLineHeight();
var targetPos = ImGui.GetCursorScreenPos();
var availableWidth = MathF.Max(rowRightLimit - targetPos.X, 0f);
var centeredY = rowTopForStats + MathF.Max((frameHeightForStats - compactHeight) * 0.5f, 0f);
ImGui.SetCursorScreenPos(new Vector2(targetPos.X, centeredY));
var performanceText = string.Empty;
var wasTruncated = false;
if (availableWidth > 0f)
{
performanceText = TruncateTextToWidth(compactPerformanceText, availableWidth, out wasTruncated);
}
ImGui.TextDisabled(performanceText);
ImGui.SetWindowFontScale(1f);
if (wasTruncated && ImGui.IsItemHovered())
{
ImGui.SetTooltip(compactPerformanceText);
}
}
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.SetNextItemWidth(MathF.Max(editBoxWidth.Invoke(), 0f));
if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForUid(pair.UserData.UID, _editComment);
_serverManager.SaveNotes();
_editEntry = string.Empty;
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_editEntry = string.Empty;
}
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
}
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
{
var textIsGid = true;
bool showUidInsteadOfName = ShowGidInsteadOfName(group);
string? groupText = _serverManager.GetNoteForGid(group.GID);
if (!showUidInsteadOfName && groupText != null)
{
if (string.IsNullOrEmpty(groupText))
{
groupText = group.GroupAliasOrGID;
}
else
{
textIsGid = false;
}
}
else
{
groupText = group.GroupAliasOrGID;
}
return (textIsGid, groupText!);
}
public (bool isUid, string text) GetPlayerText(Pair pair)
{
var textIsUid = true;
bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
if (!showUidInsteadOfName && playerText != null)
{
if (string.IsNullOrEmpty(playerText))
{
playerText = pair.UserData.AliasOrUID;
}
else
{
textIsUid = false;
}
}
else
{
playerText = pair.UserData.AliasOrUID;
}
if (_lightlessConfigService.Current.ShowCharacterNameInsteadOfNotesForVisible && pair.IsVisible && !showUidInsteadOfName)
{
playerText = pair.PlayerName;
textIsUid = false;
if (_lightlessConfigService.Current.PreferNotesOverNamesForVisible)
{
var note = pair.GetNote();
if (note != null)
{
playerText = note;
}
}
}
return (textIsUid, playerText!);
}
private string? BuildCompactPerformanceUsageText(Pair pair)
{
var config = _playerPerformanceConfigService.Current;
if (!config.ShowPerformanceIndicator || !config.ShowPerformanceUsageNextToName)
{
return null;
}
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
? pair.LastAppliedApproximateEffectiveVRAMBytes
: pair.LastAppliedApproximateVRAMBytes;
var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0
? pair.LastAppliedApproximateEffectiveTris
: pair.LastAppliedDataTris;
if (vramBytes < 0 && triangleCount < 0)
{
return null;
}
var segments = new List<string>(2);
if (vramBytes >= 0)
{
segments.Add(UiSharedService.ByteToString(vramBytes));
}
if (triangleCount >= 0)
{
segments.Add(FormatTriangleCount(triangleCount));
}
return segments.Count == 0 ? null : string.Join(" / ", segments);
}
private static string FormatTriangleCount(long triangleCount)
{
if (triangleCount < 0)
{
return string.Empty;
}
if (triangleCount >= 1_000_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris");
}
if (triangleCount >= 1_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris");
}
return $"{triangleCount} tris";
}
internal void Clear()
{
_editEntry = string.Empty;
_editComment = string.Empty;
}
internal void OpenProfile(Pair entry)
{
_mediator.Publish(new ProfileOpenStandaloneMessage(entry));
}
private bool ShowGidInsteadOfName(GroupFullInfoDto group)
{
_showIdForEntry.TryGetValue(group.GID, out var showidInsteadOfName);
return showidInsteadOfName;
}
private bool ShowUidInsteadOfName(Pair pair)
{
_showIdForEntry.TryGetValue(pair.UserData.UID, out var showidInsteadOfName);
return showidInsteadOfName;
}
private static string TruncateTextToWidth(string text, float maxWidth, out bool wasTruncated)
{
wasTruncated = false;
if (string.IsNullOrEmpty(text) || maxWidth <= 0f)
{
return string.Empty;
}
var fullWidth = ImGui.CalcTextSize(text).X;
if (fullWidth <= maxWidth)
{
return text;
}
wasTruncated = true;
const string Ellipsis = "...";
var ellipsisWidth = ImGui.CalcTextSize(Ellipsis).X;
if (ellipsisWidth >= maxWidth)
{
return Ellipsis;
}
var builder = new StringBuilder(text.Length);
var remainingWidth = maxWidth - ellipsisWidth;
foreach (var rune in text.EnumerateRunes())
{
var runeText = rune.ToString();
var runeWidth = ImGui.CalcTextSize(runeText).X;
if (runeWidth > remainingWidth)
{
break;
}
builder.Append(runeText);
remainingWidth -= runeWidth;
}
if (builder.Length == 0)
{
return Ellipsis;
}
builder.Append(Ellipsis);
return builder.ToString();
}
}