diff --git a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs
index dcdfc78..f438c45 100644
--- a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs
+++ b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs
@@ -11,6 +11,8 @@ public sealed class ChatConfig : ILightlessConfiguration
public bool ShowRulesOverlayOnOpen { get; set; } = true;
public bool ShowMessageTimestamps { get; set; } = true;
public float ChatWindowOpacity { get; set; } = .97f;
+ public bool FadeWhenUnfocused { get; set; } = false;
+ public float UnfocusedWindowOpacity { get; set; } = 0.6f;
public bool IsWindowPinned { get; set; } = false;
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
public float ChatFontScale { get; set; } = 1.0f;
diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs
index c16b380..9b4055b 100644
--- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs
+++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs
@@ -49,7 +49,8 @@ public class LightlessConfig : ILightlessConfiguration
public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
public bool PreferNotesOverNamesForVisible { get; set; } = false;
- public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
+ public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
+ public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
public float ProfileDelay { get; set; } = 1.5f;
public bool ProfilePopoutRight { get; set; } = false;
public bool ProfilesAllowNsfw { get; set; } = false;
diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs
index a7bd80c..5561bfe 100644
--- a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs
+++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs
@@ -1,36 +1,38 @@
-using LightlessSync.API.Data;
+ using LightlessSync.API.Data;
-namespace LightlessSync.PlayerData.Pairs;
+ namespace LightlessSync.PlayerData.Pairs;
-///
-/// orchestrates the lifecycle of a paired character
-///
-public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
-{
- new string Ident { get; }
- bool Initialized { get; }
- bool IsVisible { get; }
- bool ScheduledForDeletion { get; set; }
- CharacterData? LastReceivedCharacterData { get; }
- long LastAppliedDataBytes { get; }
- new string? PlayerName { get; }
- string PlayerNameHash { get; }
- uint PlayerCharacterId { get; }
- DateTime? LastDataReceivedAt { get; }
- DateTime? LastApplyAttemptAt { get; }
- DateTime? LastSuccessfulApplyAt { get; }
- string? LastFailureReason { get; }
- IReadOnlyList LastBlockingConditions { get; }
- bool IsApplying { get; }
- bool IsDownloading { get; }
- int PendingDownloadCount { get; }
- int ForbiddenDownloadCount { get; }
+ ///
+ /// orchestrates the lifecycle of a paired character
+ ///
+ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
+ {
+ new string Ident { get; }
+ bool Initialized { get; }
+ bool IsVisible { get; }
+ bool ScheduledForDeletion { get; set; }
+ CharacterData? LastReceivedCharacterData { get; }
+ long LastAppliedDataBytes { get; }
+ new string? PlayerName { get; }
+ string PlayerNameHash { get; }
+ uint PlayerCharacterId { get; }
+ DateTime? LastDataReceivedAt { get; }
+ DateTime? LastApplyAttemptAt { get; }
+ DateTime? LastSuccessfulApplyAt { get; }
+ string? LastFailureReason { get; }
+ IReadOnlyList LastBlockingConditions { get; }
+ bool IsApplying { get; }
+ bool IsDownloading { get; }
+ int PendingDownloadCount { get; }
+ int ForbiddenDownloadCount { get; }
+ DateTime? InvisibleSinceUtc { get; }
+ DateTime? VisibilityEvictionDueAtUtc { get; }
void Initialize();
- void ApplyData(CharacterData data);
- void ApplyLastReceivedData(bool forced = false);
- bool FetchPerformanceMetricsFromCache();
- void LoadCachedCharacterData(CharacterData data);
- void SetUploading(bool uploading);
- void SetPaused(bool paused);
-}
+ void ApplyData(CharacterData data);
+ void ApplyLastReceivedData(bool forced = false);
+ bool FetchPerformanceMetricsFromCache();
+ void LoadCachedCharacterData(CharacterData data);
+ void SetUploading(bool uploading);
+ void SetPaused(bool paused);
+ }
diff --git a/LightlessSync/PlayerData/Pairs/Pair.cs b/LightlessSync/PlayerData/Pairs/Pair.cs
index 7d780dd..935b705 100644
--- a/LightlessSync/PlayerData/Pairs/Pair.cs
+++ b/LightlessSync/PlayerData/Pairs/Pair.cs
@@ -194,9 +194,13 @@ public class Pair
{
var handler = TryGetHandler();
if (handler is null)
- {
return PairDebugInfo.Empty;
- }
+
+ var now = DateTime.UtcNow;
+ var dueAt = handler.VisibilityEvictionDueAtUtc;
+ var remainingSeconds = dueAt.HasValue
+ ? Math.Max(0, (dueAt.Value - now).TotalSeconds)
+ : (double?)null;
return new PairDebugInfo(
true,
@@ -206,6 +210,9 @@ public class Pair
handler.LastDataReceivedAt,
handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt,
+ handler.InvisibleSinceUtc,
+ handler.VisibilityEvictionDueAtUtc,
+ remainingSeconds,
handler.LastFailureReason,
handler.LastBlockingConditions,
handler.IsApplying,
diff --git a/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs
index 9074c82..31c3236 100644
--- a/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs
+++ b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs
@@ -8,6 +8,9 @@ public sealed record PairDebugInfo(
DateTime? LastDataReceivedAt,
DateTime? LastApplyAttemptAt,
DateTime? LastSuccessfulApplyAt,
+ DateTime? InvisibleSinceUtc,
+ DateTime? VisibilityEvictionDueAtUtc,
+ double? VisibilityEvictionRemainingSeconds,
string? LastFailureReason,
IReadOnlyList BlockingConditions,
bool IsApplying,
@@ -24,6 +27,9 @@ public sealed record PairDebugInfo(
null,
null,
null,
+ null,
+ null,
+ null,
Array.Empty(),
false,
false,
diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs
index 556dd84..706b0bc 100644
--- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs
+++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs
@@ -70,7 +70,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private DateTime? _lastSuccessfulApplyAt;
private string? _lastFailureReason;
private IReadOnlyList _lastBlockingConditions = Array.Empty();
+ private readonly object _visibilityGraceGate = new();
+ private CancellationTokenSource? _visibilityGraceCts;
+ private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
+ private DateTime? _invisibleSinceUtc;
+ private DateTime? _visibilityEvictionDueAtUtc;
+ public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
+ public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
public string Ident { get; }
public bool Initialized { get; private set; }
public bool ScheduledForDeletion { get; set; }
@@ -80,24 +87,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
get => _isVisible;
private set
{
- if (_isVisible != value)
+ if (_isVisible == value) return;
+
+ _isVisible = value;
+
+ if (!_isVisible)
{
- _isVisible = value;
- if (!_isVisible)
- {
- DisableSync();
- ResetPenumbraCollection(reason: "VisibilityLost");
- }
- else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
- {
- _ = EnsurePenumbraCollection();
- }
- var user = GetPrimaryUserData();
- Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
- EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
- Mediator.Publish(new RefreshUiMessage());
- Mediator.Publish(new VisibilityChange());
+ DisableSync();
+
+ _invisibleSinceUtc = DateTime.UtcNow;
+ _visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
+
+ StartVisibilityGraceTask();
}
+ else
+ {
+ CancelVisibilityGraceTask();
+
+ _invisibleSinceUtc = null;
+ _visibilityEvictionDueAtUtc = null;
+
+ ScheduledForDeletion = false;
+
+ if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
+ _ = EnsurePenumbraCollection();
+ }
+
+ var user = GetPrimaryUserData();
+ Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
+ EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
+ Mediator.Publish(new RefreshUiMessage());
+ Mediator.Publish(new VisibilityChange());
}
}
@@ -918,6 +938,46 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
}
+ private void CancelVisibilityGraceTask()
+ {
+ lock (_visibilityGraceGate)
+ {
+ _visibilityGraceCts?.CancelDispose();
+ _visibilityGraceCts = null;
+ }
+ }
+
+ private void StartVisibilityGraceTask()
+ {
+ CancellationToken token;
+ lock (_visibilityGraceGate)
+ {
+ _visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource();
+ token = _visibilityGraceCts.Token;
+ }
+
+ _visibilityGraceTask = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false);
+ token.ThrowIfCancellationRequested();
+ if (IsVisible) return;
+
+ ScheduledForDeletion = true;
+ ResetPenumbraCollection(reason: "VisibilityLostTimeout");
+ }
+ catch (OperationCanceledException)
+ {
+ // operation cancelled, do nothing
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier());
+ }
+ }, CancellationToken.None);
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -936,7 +996,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_downloadCancellationTokenSource = null;
_downloadManager.Dispose();
_charaHandler?.Dispose();
+ CancelVisibilityGraceTask();
_charaHandler = null;
+ _invisibleSinceUtc = null;
+ _visibilityEvictionDueAtUtc = null;
if (!string.IsNullOrEmpty(name))
{
@@ -1265,6 +1328,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
private Task? _pairDownloadTask;
+ private Task _visibilityGraceTask;
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData,
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs
index f490804..ec05ee7 100644
--- a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs
+++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs
@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
{
private readonly object _gate = new();
private readonly object _pendingGate = new();
+ private readonly object _visibilityGate = new();
private readonly Dictionary _entriesByIdent = new(StringComparer.Ordinal);
+ private readonly Dictionary _pendingInvisibleEvictions = new(StringComparer.Ordinal);
private readonly Dictionary _entriesByHandler = new(ReferenceEqualityComparer.Instance);
private readonly IPairHandlerAdapterFactory _handlerFactory;
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult.Ok(registration.PairIdent);
}
+ private PairOperationResult CancelAllInvisibleEvictions()
+ {
+ List snapshot;
+ lock (_visibilityGate)
+ {
+ snapshot = [.. _pendingInvisibleEvictions.Values];
+ _pendingInvisibleEvictions.Clear();
+ }
+
+ List? errors = null;
+
+ foreach (var cts in snapshot)
+ {
+ try { cts.Cancel(); }
+ catch (Exception ex)
+ {
+ (errors ??= new List()).Add($"Cancel: {ex.Message}");
+ }
+
+ try { cts.Dispose(); }
+ catch (Exception ex)
+ {
+ (errors ??= new List()).Add($"Dispose: {ex.Message}");
+ }
+ }
+
+ return errors is null
+ ? PairOperationResult.Ok()
+ : PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
+ }
+
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
{
if (registration.CharacterIdent is null)
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate)
{
handlers = _entriesByHandler.Keys.ToList();
+ CancelAllInvisibleEvictions();
_entriesByIdent.Clear();
_entriesByHandler.Clear();
}
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate)
{
handlers = _entriesByHandler.Keys.ToList();
+ CancelAllInvisibleEvictions();
_entriesByIdent.Clear();
_entriesByHandler.Clear();
}
diff --git a/LightlessSync/Services/CommandManagerService.cs b/LightlessSync/Services/CommandManagerService.cs
index d42a865..0014b3a 100644
--- a/LightlessSync/Services/CommandManagerService.cs
+++ b/LightlessSync/Services/CommandManagerService.cs
@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
- "\t /light finder - Opens the Lightless Chat window"
+ "\t /light chat - Opens the Lightless Chat window"
});
}
diff --git a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs
index f360ba3..20d9a8f 100644
--- a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs
+++ b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs
@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
private const string TextureSegment = "/texture/";
private const string MaterialSegment = "/material/";
- private const uint NormalSamplerId = 0x0C5EC1F1u;
- private const uint IndexSamplerId = 0x565F8FD8u;
- private const uint SpecularSamplerId = 0x2B99E025u;
- private const uint DiffuseSamplerId = 0x115306BEu;
- private const uint MaskSamplerId = 0x8A4E82B6u;
+ private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
+ private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
+ private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
+ private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
+ private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
public TextureMetadataHelper(ILogger logger, IDataManager dataManager)
{
diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs
index cd758f5..b1195b4 100644
--- a/LightlessSync/UI/CompactUI.cs
+++ b/LightlessSync/UI/CompactUI.cs
@@ -843,12 +843,16 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
- var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
-
- if (allOnlineNotTaggedPairs.Count > 0)
- {
+ if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
+ var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
- _configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
+ TagHandler.CustomOnlineTag,
+ filteredOnlineEntries,
+ allOnlineNotTaggedPairs));
+ } else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
+ var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
+ drawFolders.Add(_drawEntityFactory.CreateTagFolder(
+ TagHandler.CustomAllTag,
onlineNotTaggedPairs,
allOnlineNotTaggedPairs));
}
@@ -885,7 +889,7 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
- private bool PassesFilter(PairUiEntry entry, string filter)
+ private static bool PassesFilter(PairUiEntry entry, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
@@ -946,6 +950,17 @@ public class CompactUi : WindowMediatorSubscriberBase
};
}
+ private ImmutableList SortOnlineEntries(IEnumerable entries)
+ {
+ var entryList = entries.ToList();
+ return _configService.Current.OnlinePairSortMode switch
+ {
+ OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
+ OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
+ _ => SortEntries(entryList),
+ };
+ }
+
private ImmutableList SortVisibleByMetric(IEnumerable entries, Func selector)
{
return [.. entries
diff --git a/LightlessSync/UI/Components/DrawFolderBase.cs b/LightlessSync/UI/Components/DrawFolderBase.cs
index 40330c7..0532da9 100644
--- a/LightlessSync/UI/Components/DrawFolderBase.cs
+++ b/LightlessSync/UI/Components/DrawFolderBase.cs
@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using System.Collections.Immutable;
-using LightlessSync.UI;
using LightlessSync.UI.Style;
+using OtterGui.Text;
namespace LightlessSync.UI.Components;
@@ -113,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
if (DrawPairs.Any())
{
- foreach (var item in DrawPairs)
+ using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
+ while (clipper.Step())
{
- item.DrawPairedClient();
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ DrawPairs[i].DrawPairedClient();
+ }
}
}
else
diff --git a/LightlessSync/UI/Components/DrawFolderTag.cs b/LightlessSync/UI/Components/DrawFolderTag.cs
index dcba0d4..b91617a 100644
--- a/LightlessSync/UI/Components/DrawFolderTag.cs
+++ b/LightlessSync/UI/Components/DrawFolderTag.cs
@@ -169,11 +169,16 @@ public class DrawFolderTag : DrawFolderBase
protected override float DrawRightSide(float currentRightSideX)
{
- if (_id == TagHandler.CustomVisibleTag)
+ if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
{
return DrawVisibleFilter(currentRightSideX);
}
+ if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
+ {
+ return DrawOnlineFilter(currentRightSideX);
+ }
+
if (!RenderPause)
{
return currentRightSideX;
@@ -254,7 +259,7 @@ public class DrawFolderTag : DrawFolderBase
foreach (VisiblePairSortMode mode in Enum.GetValues())
{
var selected = _configService.Current.VisiblePairSortMode == mode;
- if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
+ if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
{
if (!selected)
{
@@ -273,7 +278,49 @@ public class DrawFolderTag : DrawFolderBase
return buttonStart - spacingX;
}
- private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
+ private float DrawOnlineFilter(float currentRightSideX)
+ {
+ var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
+ var spacingX = ImGui.GetStyle().ItemSpacing.X;
+ var buttonStart = currentRightSideX - buttonSize.X;
+
+ ImGui.SameLine(buttonStart);
+ if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
+ {
+ SuppressNextRowToggle();
+ ImGui.OpenPopup($"online-filter-{_id}");
+ }
+
+ UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
+
+ if (ImGui.BeginPopup($"online-filter-{_id}"))
+ {
+ ImGui.TextUnformatted("Online Pair Ordering");
+ ImGui.Separator();
+
+ foreach (OnlinePairSortMode mode in Enum.GetValues())
+ {
+ var selected = _configService.Current.OnlinePairSortMode == mode;
+ if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
+ {
+ if (!selected)
+ {
+ _configService.Current.OnlinePairSortMode = mode;
+ _configService.Save();
+ _mediator.Publish(new RefreshUiMessage());
+ }
+
+ ImGui.CloseCurrentPopup();
+ }
+ }
+
+ ImGui.EndPopup();
+ }
+
+ return buttonStart - spacingX;
+ }
+
+ private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
{
VisiblePairSortMode.Alphabetical => "Alphabetical",
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
@@ -282,4 +329,11 @@ public class DrawFolderTag : DrawFolderBase
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
+
+ private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
+ {
+ OnlinePairSortMode.Alphabetical => "Alphabetical",
+ OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
+ _ => "Default",
+ };
}
\ No newline at end of file
diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs
index ca74bc9..22911cb 100644
--- a/LightlessSync/UI/LightFinderUI.cs
+++ b/LightlessSync/UI/LightFinderUI.cs
@@ -301,6 +301,14 @@ namespace LightlessSync.UI
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting;
+ if (isBroadcasting)
+ {
+ var warningColor = UIColors.Get("LightlessYellow");
+ _uiSharedService.DrawNoteLine("! ", warningColor,
+ new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
+ ImGuiHelpers.ScaledDummy(0.2f);
+ }
+
if (isBroadcasting)
ImGui.BeginDisabled();
diff --git a/LightlessSync/UI/Models/OnlinePairSortMode.cs b/LightlessSync/UI/Models/OnlinePairSortMode.cs
new file mode 100644
index 0000000..ff85b9c
--- /dev/null
+++ b/LightlessSync/UI/Models/OnlinePairSortMode.cs
@@ -0,0 +1,7 @@
+namespace LightlessSync.UI.Models;
+
+public enum OnlinePairSortMode
+{
+ Alphabetical = 0,
+ PreferredDirectPairs = 1,
+}
diff --git a/LightlessSync/UI/Models/VisiblePairSortMode.cs b/LightlessSync/UI/Models/VisiblePairSortMode.cs
index fcb1d65..ec133b9 100644
--- a/LightlessSync/UI/Models/VisiblePairSortMode.cs
+++ b/LightlessSync/UI/Models/VisiblePairSortMode.cs
@@ -2,10 +2,9 @@ namespace LightlessSync.UI.Models;
public enum VisiblePairSortMode
{
- Default = 0,
- Alphabetical = 1,
- VramUsage = 2,
- EffectiveVramUsage = 3,
- TriangleCount = 4,
- PreferredDirectPairs = 5,
+ Alphabetical = 0,
+ VramUsage = 1,
+ EffectiveVramUsage = 2,
+ TriangleCount = 3,
+ PreferredDirectPairs = 4,
}
diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs
index 4ce64ac..c30d5fa 100644
--- a/LightlessSync/UI/SettingsUi.cs
+++ b/LightlessSync/UI/SettingsUi.cs
@@ -1463,7 +1463,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
+ DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
+ DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
+
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
ImGui.EndTable();
}
@@ -1698,6 +1701,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
}
+ private static string? FormatCountdown(double? remainingSeconds)
+ {
+ if (!remainingSeconds.HasValue)
+ return "No";
+
+ var secs = Math.Max(0, remainingSeconds.Value);
+ var t = TimeSpan.FromSeconds(secs);
+
+ return t.TotalHours >= 1
+ ? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
+ : $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
+ }
+
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs
index 2f215a1..0586c06 100644
--- a/LightlessSync/UI/SyncshellFinderUI.cs
+++ b/LightlessSync/UI/SyncshellFinderUI.cs
@@ -350,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
? shell.Group.Alias
: shell.Group.GID;
+ var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX();
- float availWidth = ImGui.GetContentRegionAvail().X;
- float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
+ float availW = ImGui.GetContentRegionAvail().X;
ImGui.BeginGroup();
@@ -364,13 +364,45 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
}
- ImGui.SameLine();
- float rightX = startX + availWidth - rightTextW;
- var pos = ImGui.GetCursorPos();
- ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale));
- ImGui.TextUnformatted(broadcasterName);
- if (ImGui.IsItemHovered())
- ImGui.SetTooltip("Broadcaster of the syncshell.");
+ float nameRightX = ImGui.GetItemRectMax().X;
+
+ var regionMinScreen = ImGui.GetCursorScreenPos();
+ float regionRightX = regionMinScreen.X + availW;
+
+ float minBroadcasterX = nameRightX + style.ItemSpacing.X;
+
+ float maxBroadcasterWidth = regionRightX - minBroadcasterX;
+
+ string broadcasterToShow = broadcasterName;
+
+ if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
+ {
+ float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
+ string toolTip;
+
+ if (bcFullWidth > maxBroadcasterWidth)
+ {
+ broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
+ toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
+ }
+ else
+ {
+ toolTip = "Broadcaster of the syncshell.";
+ }
+
+ float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
+
+ float broadX = regionRightX - bcWidth;
+
+ broadX = MathF.Max(broadX, minBroadcasterX);
+
+ ImGui.SameLine();
+ var curPos = ImGui.GetCursorPos();
+ ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
+ ImGui.TextUnformatted(broadcasterToShow);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(toolTip);
+ }
ImGui.EndGroup();
@@ -590,6 +622,40 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float widthUsed = cursorLocalX - baseLocal.X;
return (widthUsed, rowHeight);
}
+ private static string TruncateTextToWidth(string text, float maxWidth)
+ {
+ if (string.IsNullOrEmpty(text))
+ return text;
+
+ const string ellipsis = "...";
+ float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
+
+ if (maxWidth <= ellipsisWidth)
+ return ellipsis;
+
+ int low = 0;
+ int high = text.Length;
+ string best = ellipsis;
+
+ while (low <= high)
+ {
+ int mid = (low + high) / 2;
+ string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
+ float width = ImGui.CalcTextSize(candidate).X;
+
+ if (width <= maxWidth)
+ {
+ best = candidate;
+ low = mid + 1;
+ }
+ else
+ {
+ high = mid - 1;
+ }
+ }
+
+ return best;
+ }
private IDalamudTextureWrap? GetIconWrap(uint iconId)
{
diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs
index d45427c..396e63c 100644
--- a/LightlessSync/UI/ZoneChatUi.cs
+++ b/LightlessSync/UI/ZoneChatUi.cs
@@ -11,9 +11,11 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Chat;
+using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services;
+using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils;
@@ -29,10 +31,13 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
private const float DefaultWindowOpacity = .97f;
+ private const float DefaultUnfocusedWindowOpacity = 0.6f;
private const float MinWindowOpacity = 0.05f;
private const float MaxWindowOpacity = 1f;
private const float MinChatFontScale = 0.75f;
private const float MaxChatFontScale = 1.5f;
+ private const float UnfocusedFadeOutSpeed = 0.22f;
+ private const float FocusFadeInSpeed = 2.0f;
private const int ReportReasonMaxLength = 500;
private const int ReportContextMaxLength = 1000;
private const int MaxChannelNoteTabLength = 25;
@@ -40,6 +45,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiSharedService;
private readonly ZoneChatService _zoneChatService;
private readonly PairUiService _pairUiService;
+ private readonly LightFinderService _lightFinderService;
private readonly LightlessProfileManager _profileManager;
private readonly ApiController _apiController;
private readonly ChatConfigService _chatConfigService;
@@ -49,16 +55,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private readonly Dictionary _draftMessages = new(StringComparer.Ordinal);
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
private float _currentWindowOpacity = DefaultWindowOpacity;
+ private float _baseWindowOpacity = DefaultWindowOpacity;
private bool _isWindowPinned;
private bool _showRulesOverlay;
private bool _refocusChatInput;
private string? _refocusChatInputKey;
+ private bool _isWindowFocused = true;
+ private int _titleBarStylePopCount;
private string? _selectedChannelKey;
private bool _scrollToBottom = true;
private float? _pendingChannelScroll;
private float _channelScroll;
private float _channelScrollMax;
+ private readonly SeluneBrush _seluneBrush = new();
private ChatChannelSnapshot? _reportTargetChannel;
private ChatMessageEntry? _reportTargetMessage;
private string _reportReason = string.Empty;
@@ -79,6 +89,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
UiSharedService uiSharedService,
ZoneChatService zoneChatService,
PairUiService pairUiService,
+ LightFinderService lightFinderService,
LightlessProfileManager profileManager,
ChatConfigService chatConfigService,
ServerConfigurationManager serverConfigurationManager,
@@ -91,6 +102,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_uiSharedService = uiSharedService;
_zoneChatService = zoneChatService;
_pairUiService = pairUiService;
+ _lightFinderService = lightFinderService;
_profileManager = profileManager;
_chatConfigService = chatConfigService;
_serverConfigurationManager = serverConfigurationManager;
@@ -124,8 +136,25 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{
RefreshWindowFlags();
base.PreDraw();
- _currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
+ var config = _chatConfigService.Current;
+ var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
+ _baseWindowOpacity = baseOpacity;
+
+ if (config.FadeWhenUnfocused)
+ {
+ var unfocusedOpacity = Math.Clamp(config.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
+ var targetOpacity = _isWindowFocused ? baseOpacity : Math.Min(baseOpacity, unfocusedOpacity);
+ var delta = ImGui.GetIO().DeltaTime;
+ var speed = _isWindowFocused ? FocusFadeInSpeed : UnfocusedFadeOutSpeed;
+ _currentWindowOpacity = MoveTowards(_currentWindowOpacity, targetOpacity, speed * delta);
+ }
+ else
+ {
+ _currentWindowOpacity = baseOpacity;
+ }
+
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
+ PushTitleBarFadeColors(_currentWindowOpacity);
}
private void UpdateHideState()
@@ -179,8 +208,36 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
protected override void DrawInternal()
{
+ if (_titleBarStylePopCount > 0)
+ {
+ ImGui.PopStyleColor(_titleBarStylePopCount);
+ _titleBarStylePopCount = 0;
+ }
+
+ var config = _chatConfigService.Current;
+ var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
+ var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
+ if (config.FadeWhenUnfocused && isHovered && !isFocused)
+ {
+ ImGui.SetWindowFocus();
+ }
+
+ _isWindowFocused = config.FadeWhenUnfocused ? (isFocused || isHovered) : isFocused;
+
+ var contentAlpha = 1f;
+ if (config.FadeWhenUnfocused)
+ {
+ var baseOpacity = MathF.Max(_baseWindowOpacity, 0.001f);
+ contentAlpha = Math.Clamp(_currentWindowOpacity / baseOpacity, 0f, 1f);
+ }
+
+ using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, contentAlpha);
+ var drawList = ImGui.GetWindowDrawList();
+ var windowPos = ImGui.GetWindowPos();
+ var windowSize = ImGui.GetWindowSize();
+ using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
- childBgColor.W *= _currentWindowOpacity;
+ childBgColor.W *= _baseWindowOpacity;
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
DrawConnectionControls();
@@ -192,36 +249,58 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextWrapped("No chat channels available.");
ImGui.PopStyleColor();
- return;
}
-
- EnsureSelectedChannel(channels);
- CleanupDrafts(channels);
-
- DrawChannelButtons(channels);
-
- if (_selectedChannelKey is null)
- return;
-
- var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
- if (activeChannel.Equals(default(ChatChannelSnapshot)))
+ else
{
- activeChannel = channels[0];
- _selectedChannelKey = activeChannel.Key;
+ EnsureSelectedChannel(channels);
+ CleanupDrafts(channels);
+
+ DrawChannelButtons(channels);
+
+ if (_selectedChannelKey is null)
+ {
+ selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
+ return;
+ }
+
+ var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
+ if (activeChannel.Equals(default(ChatChannelSnapshot)))
+ {
+ activeChannel = channels[0];
+ _selectedChannelKey = activeChannel.Key;
+ }
+
+ _zoneChatService.SetActiveChannel(activeChannel.Key);
+
+ DrawHeader(activeChannel);
+ ImGui.Separator();
+ DrawMessageArea(activeChannel, _currentWindowOpacity);
+ ImGui.Separator();
+ DrawInput(activeChannel);
}
- _zoneChatService.SetActiveChannel(activeChannel.Key);
-
- DrawHeader(activeChannel);
- ImGui.Separator();
- DrawMessageArea(activeChannel, _currentWindowOpacity);
- ImGui.Separator();
- DrawInput(activeChannel);
-
if (_showRulesOverlay)
{
DrawRulesOverlay();
}
+
+ selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
+ }
+
+ private void PushTitleBarFadeColors(float opacity)
+ {
+ _titleBarStylePopCount = 0;
+ var alpha = Math.Clamp(opacity, 0f, 1f);
+ var colors = ImGui.GetStyle().Colors;
+
+ var titleBg = colors[(int)ImGuiCol.TitleBg];
+ var titleBgActive = colors[(int)ImGuiCol.TitleBgActive];
+ var titleBgCollapsed = colors[(int)ImGuiCol.TitleBgCollapsed];
+
+ ImGui.PushStyleColor(ImGuiCol.TitleBg, new Vector4(titleBg.X, titleBg.Y, titleBg.Z, titleBg.W * alpha));
+ ImGui.PushStyleColor(ImGuiCol.TitleBgActive, new Vector4(titleBgActive.X, titleBgActive.Y, titleBgActive.Z, titleBgActive.W * alpha));
+ ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, new Vector4(titleBgCollapsed.X, titleBgCollapsed.Y, titleBgCollapsed.Z, titleBgCollapsed.W * alpha));
+ _titleBarStylePopCount = 3;
}
private void DrawHeader(ChatChannelSnapshot channel)
@@ -1119,18 +1198,56 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
var groupSize = ImGui.GetItemRectSize();
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
+ var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X;
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
- var blockWidth = rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
+ var blockWidth = lightfinderButtonWidth + style.ItemSpacing.X + rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
? contentRightX - blockWidth
: minBlockX;
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
- var rulesPos = new Vector2(desiredBlockX, cursorStart.Y);
- var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
+ var lightfinderPos = new Vector2(desiredBlockX, cursorStart.Y);
+ var rulesPos = new Vector2(lightfinderPos.X + lightfinderButtonWidth + style.ItemSpacing.X, cursorStart.Y);
+ var settingsPos = new Vector2(rulesPos.X + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
+ ImGui.SameLine();
+ ImGui.SetCursorPos(lightfinderPos);
+ var lightfinderEnabled = _lightFinderService.IsBroadcasting;
+ var lightfinderColor = lightfinderEnabled ? UIColors.Get("LightlessGreen") : ImGuiColors.DalamudGrey3;
+ var lightfinderButtonSize = new Vector2(lightfinderButtonWidth, ImGui.GetFrameHeight());
+ ImGui.InvisibleButton("zone_chat_lightfinder_button", lightfinderButtonSize);
+ var lightfinderMin = ImGui.GetItemRectMin();
+ var lightfinderMax = ImGui.GetItemRectMax();
+ var iconSize = _uiSharedService.GetIconSize(FontAwesomeIcon.PersonCirclePlus);
+ var iconPos = new Vector2(
+ lightfinderMin.X + (lightfinderButtonSize.X - iconSize.X) * 0.5f,
+ lightfinderMin.Y + (lightfinderButtonSize.Y - iconSize.Y) * 0.5f);
+ using (_uiSharedService.IconFont.Push())
+ {
+ ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(lightfinderColor), FontAwesomeIcon.PersonCirclePlus.ToIconString());
+ }
+
+ if (ImGui.IsItemClicked())
+ {
+ Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
+ }
+ if (ImGui.IsItemHovered())
+ {
+ var padding = new Vector2(8f * ImGuiHelpers.GlobalScale);
+ Selune.RegisterHighlight(
+ lightfinderMin - padding,
+ lightfinderMax + padding,
+ SeluneHighlightMode.Point,
+ exactSize: true,
+ clipToElement: true,
+ clipPadding: padding,
+ highlightColorOverride: lightfinderColor,
+ highlightAlphaOverride: 0.2f);
+ ImGui.SetTooltip("If Lightfinder is enabled, you will be able to see the character names of other Lightfinder users in the same zone when they send a message.");
+ }
+
ImGui.SameLine();
ImGui.SetCursorPos(rulesPos);
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
@@ -1376,9 +1493,55 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
}
+ var fadeUnfocused = chatConfig.FadeWhenUnfocused;
+ if (ImGui.Checkbox("Fade window when unfocused", ref fadeUnfocused))
+ {
+ chatConfig.FadeWhenUnfocused = fadeUnfocused;
+ _chatConfigService.Save();
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("When enabled, the chat window fades after it loses focus.\nHovering the window restores focus.");
+ }
+
+ ImGui.BeginDisabled(!fadeUnfocused);
+ var unfocusedOpacity = Math.Clamp(chatConfig.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
+ var unfocusedChanged = ImGui.SliderFloat("Unfocused transparency", ref unfocusedOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f");
+ var resetUnfocused = ImGui.IsItemClicked(ImGuiMouseButton.Right);
+ if (resetUnfocused)
+ {
+ unfocusedOpacity = DefaultUnfocusedWindowOpacity;
+ unfocusedChanged = true;
+ }
+ if (unfocusedChanged)
+ {
+ chatConfig.UnfocusedWindowOpacity = unfocusedOpacity;
+ _chatConfigService.Save();
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Target transparency while the chat window is unfocused.\nRight-click to reset to default.");
+ }
+ ImGui.EndDisabled();
+
ImGui.EndPopup();
}
+ private static float MoveTowards(float current, float target, float maxDelta)
+ {
+ if (current < target)
+ {
+ return MathF.Min(current + maxDelta, target);
+ }
+
+ if (current > target)
+ {
+ return MathF.Max(current - maxDelta, target);
+ }
+
+ return target;
+ }
+
private void ToggleChatConnection(bool currentlyEnabled)
{
_ = Task.Run(async () =>