From 52b0fa7c67b8d135fc31fa53695f5b747503f9c8 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sat, 23 May 2026 20:50:54 +0200 Subject: [PATCH] fix(ui): wire chat send and fix sidebar icons, channel pill, scrollbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five smoke bugs from the first in-game test: 1. Sidebar showed fa-comment for every tab because the resolve path only honoured tab.Icon. Channel-type fallback restored — auto-tell tabs render the envelope, the rest map their first SelectedChannels key onto FontAwesome (Linkshells → link, FC → users, Party → user-friends, System/Echo → cog, emotes → comments). 2. InputBar's channel pill read from tab.Channel (the saved default), which is null on most non-FC tabs and rendered as "—". The pill now reads tab.CurrentChannel.Channel first so the runtime input state surfaces on every tab, with the saved default as a second fallback. 3. MessageList was making its own ImRaii.Child inside the main-area child MainWindow already owns. That nested scroll created the second scrollbar on the outer window. The component now lays out directly into the parent's scroll region. 4. The input field reserved 90px for the three FontAwesome buttons, which clipped them on standard frame padding. Reserve raised to 130px so the trailing buttons fully render. 5. Pressing Enter dropped the buffer — there was no send wiring. The field now uses ImGuiInputTextFlags.EnterReturnsTrue and routes the pending message through GameFunctions.ChatBox.SendMessage. Lines that don't start with a slash get the active channel's prefix prepended so typing in /fc lands on the FC channel instead of the current game-side default. InputBar gains an ILogger for the send-failure path; the DI registration in PluginHostFactory is updated to match. --- HellionChat/PluginHostFactory.cs | 3 +- HellionChat/Ui/Components/InputBar.cs | 78 +++++++++++++++++++++--- HellionChat/Ui/Components/MessageList.cs | 13 ++-- HellionChat/Ui/Components/Sidebar.cs | 47 ++++++++++++++ 4 files changed, 122 insertions(+), 19 deletions(-) diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index bd13342..e7d4721 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -138,7 +138,8 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService>() )); services.AddSingleton(sp => new Ui.Components.StatusBar( sp.GetRequiredService(), diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index a10cca3..0601264 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -3,28 +3,33 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using HellionChat.Code; +using HellionChat.GameFunctions; using HellionChat.Themes; using HellionChat.Ui.StyleEngine; using HellionChat.Util; +using Microsoft.Extensions.Logging; namespace HellionChat.Ui.Components; // Bottom input row: channel pill, text field, quick buttons. Channel pill // recolours by tab type — cyan accent for a normal channel, ember accent -// for a tell. Send wiring lands when the main window assembles the -// components; for now this layer only handles buffer state and the symbol -// picker overlay. +// for a tell. Enter on the input field sends through ChatBox; messages +// that don't already start with a slash get the active channel's prefix +// prepended so a typed line in /fc reaches free-company chat instead of +// the current game-side channel. internal sealed class InputBar { public const float Height = 32f; private const float PillHeight = 22f; private const float PillPaddingX = 8f; private const int BufferCapacity = 500; + private const float QuickButtonsReserve = 130f; private readonly SymbolPicker _symbolPicker; private readonly FontManager _fonts; private readonly ThemeRegistry _themes; private readonly TokenResolver _resolver; + private readonly ILogger _logger; private string _pendingMessage = string.Empty; @@ -32,13 +37,15 @@ internal sealed class InputBar SymbolPicker symbolPicker, FontManager fonts, ThemeRegistry themes, - TokenResolver resolver + TokenResolver resolver, + ILogger logger ) { _symbolPicker = symbolPicker; _fonts = fonts; _themes = themes; _resolver = resolver; + _logger = logger; } public string PendingMessage => _pendingMessage; @@ -62,7 +69,7 @@ internal sealed class InputBar DrawChannelPill(activeTab, isTell, pillAbgr, pillTextAbgr); ImGui.SameLine(); - DrawInputField(); + DrawInputField(activeTab); ImGui.SameLine(); DrawQuickButtons(); @@ -77,8 +84,17 @@ internal sealed class InputBar { if (isTell && tab?.TellTarget is { } t && t.IsSet()) return $"→ {t.Name}"; - if (tab?.Channel is { } ch) - return ch.ToChatType().Name(); + + // CurrentChannel carries the runtime input state; Tab.Channel is the + // saved default and is null for most non-FC tabs, which produced + // the "—" placeholder users saw. + var current = tab?.CurrentChannel?.Channel ?? InputChannel.Invalid; + if (current != InputChannel.Invalid) + return current.ToChatType().Name(); + + if (tab?.Channel is { } saved) + return saved.ToChatType().Name(); + return "—"; } @@ -98,10 +114,52 @@ internal sealed class InputBar ImGui.Dummy(new Vector2(width, PillHeight)); } - private void DrawInputField() + private void DrawInputField(Tab? activeTab) { - ImGui.SetNextItemWidth(-90f); - ImGui.InputText("##hellion-input", ref _pendingMessage, BufferCapacity); + ImGui.SetNextItemWidth(-QuickButtonsReserve); + if ( + ImGui.InputText( + "##hellion-input", + ref _pendingMessage, + BufferCapacity, + ImGuiInputTextFlags.EnterReturnsTrue + ) + ) + { + TrySend(activeTab); + } + } + + private void TrySend(Tab? activeTab) + { + var text = _pendingMessage.Trim(); + if (string.IsNullOrEmpty(text)) + return; + + // Slash commands route through verbatim — the game's chat parser + // handles /tell, /fc, /hellion etc. on its own. Other text gets + // the active channel's prefix so the line lands on the channel + // the user is reading instead of the game-side default. + string toSend; + if (text.StartsWith('/')) + { + toSend = text; + } + else + { + var current = activeTab?.CurrentChannel?.Channel ?? InputChannel.Invalid; + toSend = current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}"; + } + + try + { + ChatBox.SendMessage(toSend); + _pendingMessage = string.Empty; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send chat message ({Length} chars)", toSend.Length); + } } private void DrawQuickButtons() diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index 1bb7c67..7bb09b2 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -37,10 +37,9 @@ internal sealed class MessageList return; } - using var child = ImRaii.Child("##hellion-messages", new Vector2(-1, -1)); - if (!child.Success) - return; - + // No own ImRaii.Child here — MainWindow already wraps the message + // area in one. Nesting would give the window two stacked scrolls + // and a runaway content-height computation. var theme = _themes.Active; var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); @@ -49,10 +48,8 @@ internal sealed class MessageList var compact = Plugin.Config.UseCompactDensity; // Track whether the user was pinned to the bottom before this frame - // so newly arriving rows do not yank them up — the standard - // chat-window expectation. Read the scroll state before drawing - // anything inside the child so the comparison is against the - // previous frame's max. + // so newly arriving rows do not yank them up. The check runs against + // the parent child's scroll state, which is the one MainWindow owns. var pinnedToBottom = ImGui.GetScrollY() >= ImGui.GetScrollMaxY() - 1f; if (compact) diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index e1f1e68..3b3bda0 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; +using HellionChat.Code; using HellionChat.Themes; using HellionChat.Ui.StyleEngine; using HellionChat.Util; @@ -164,9 +165,55 @@ internal sealed class Sidebar !string.IsNullOrWhiteSpace(tab.Icon) && IconByName.TryGetValue(tab.Icon, out var mapped) ) return mapped; + + // Auto-tell tabs always show the envelope, regardless of what their + // SelectedChannels filter is set to. + if (tab.IsTempTab) + return FontAwesomeIcon.Envelope; + + // Channel-type fallback. The v1.5.6 TabIconGlyphResolver did the + // same thing — picks the first selected channel and maps its + // ChatType to a category icon so tabs without a user-set icon + // still look distinct. + if (tab.SelectedChannels.Count > 0) + return ResolveByChannelType(tab.SelectedChannels.Keys.First()); + return FontAwesomeIcon.Comment; } + private static FontAwesomeIcon ResolveByChannelType(ChatType type) => + type switch + { + ChatType.TellIncoming or ChatType.TellOutgoing => FontAwesomeIcon.Envelope, + ChatType.FreeCompany + or ChatType.FreeCompanyAnnouncement + or ChatType.FreeCompanyLoginLogout => FontAwesomeIcon.Users, + ChatType.Linkshell1 + or ChatType.Linkshell2 + or ChatType.Linkshell3 + or ChatType.Linkshell4 + or ChatType.Linkshell5 + or ChatType.Linkshell6 + or ChatType.Linkshell7 + or ChatType.Linkshell8 + or ChatType.CrossLinkshell1 + or ChatType.CrossLinkshell2 + or ChatType.CrossLinkshell3 + or ChatType.CrossLinkshell4 + or ChatType.CrossLinkshell5 + or ChatType.CrossLinkshell6 + or ChatType.CrossLinkshell7 + or ChatType.CrossLinkshell8 => FontAwesomeIcon.Link, + ChatType.Party or ChatType.CrossParty => FontAwesomeIcon.UserFriends, + ChatType.Alliance => FontAwesomeIcon.Users, + ChatType.NoviceNetwork or ChatType.NoviceNetworkSystem => FontAwesomeIcon.Users, + ChatType.PvpTeam or ChatType.PvpTeamAnnouncement or ChatType.PvpTeamLoginLogout => + FontAwesomeIcon.Users, + ChatType.System or ChatType.Echo => FontAwesomeIcon.Cog, + ChatType.CustomEmote or ChatType.StandardEmote => FontAwesomeIcon.Comments, + _ => FontAwesomeIcon.Comment, + }; + private void LogPopOutStub(Tab tab) { // The channel-popout pool is built in a later cycle; logging here