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/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 () =>