From 36afce21e50187809cba66db1ebf052831d52ff4 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sat, 23 May 2026 19:17:25 +0200 Subject: [PATCH] feat(ui): add InputBar with channel pill and symbol picker overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Channel pill picks Token.AccentEmber when the tab is a tell (matched by IsTempTab + a set TellTarget) and Token.AccentPrimary otherwise, so the tinted background reads as the channel type at a glance. SymbolPicker runs as an overlay popup — the inserted fragment splices straight into the pending buffer up to a 500-char cap. Send wiring and the settings button arrive when the main window assembles the components. --- HellionChat/PluginHostFactory.cs | 6 ++ HellionChat/Ui/Components/InputBar.cs | 145 ++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 HellionChat/Ui/Components/InputBar.cs diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 247494f..23bd483 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -135,6 +135,12 @@ internal static class PluginHostFactory sp.GetRequiredService() )); services.AddSingleton(_ => new Ui.Components.SymbolPicker()); + services.AddSingleton(sp => new Ui.Components.InputBar( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService() + )); services.AddSingleton(sp => new Integrations.FailedTellNotifier( sp.GetRequiredService>() )); diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs new file mode 100644 index 0000000..be37f9c --- /dev/null +++ b/HellionChat/Ui/Components/InputBar.cs @@ -0,0 +1,145 @@ +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; + +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. +internal sealed class InputBar +{ + private const float Height = 32f; + private const float PillHeight = 22f; + private const float PillPaddingX = 8f; + private const int BufferCapacity = 500; + + private readonly SymbolPicker _symbolPicker; + private readonly FontManager _fonts; + private readonly ThemeRegistry _themes; + private readonly TokenResolver _resolver; + + private string _pendingMessage = string.Empty; + + public InputBar( + SymbolPicker symbolPicker, + FontManager fonts, + ThemeRegistry themes, + TokenResolver resolver + ) + { + _symbolPicker = symbolPicker; + _fonts = fonts; + _themes = themes; + _resolver = resolver; + } + + public string PendingMessage => _pendingMessage; + + public void ClearBuffer() => _pendingMessage = string.Empty; + + public void Draw(Tab? activeTab) + { + if (!_fonts.FontsReady) + { + ImGui.Dummy(new Vector2(0, Height)); + return; + } + + var theme = _themes.Active; + var isTell = activeTab is { IsTempTab: true, TellTarget: { } target } && target.IsSet(); + var pillToken = isTell ? Token.AccentEmber : Token.AccentPrimary; + var pillRgba = _resolver.Resolve(pillToken, theme.Colors); + var pillAbgr = ColourUtil.RgbaToAbgr(pillRgba); + var pillTextAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + + DrawChannelPill(activeTab, isTell, pillAbgr, pillTextAbgr); + ImGui.SameLine(); + DrawInputField(); + ImGui.SameLine(); + DrawQuickButtons(); + + // SymbolPicker popup is rendered last so it can splice its fragment + // straight into the pending buffer. + var inserted = _symbolPicker.DrawAndConsume(); + if (inserted is not null && _pendingMessage.Length + inserted.Length <= BufferCapacity) + _pendingMessage += inserted; + } + + private static string ResolvePillLabel(Tab? tab, bool isTell) + { + if (isTell && tab?.TellTarget is { } t && t.IsSet()) + return $"→ {t.Name}"; + if (tab?.Channel is { } ch) + return ch.ToChatType().Name(); + return "—"; + } + + private void DrawChannelPill(Tab? tab, bool isTell, uint pillAbgr, uint textAbgr) + { + var label = ResolvePillLabel(tab, isTell); + var labelSize = ImGui.CalcTextSize(label); + var width = labelSize.X + PillPaddingX * 2; + var origin = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + var max = origin + new Vector2(width, PillHeight); + + dl.AddRectFilled(origin, max, pillAbgr, 6f); + dl.AddText(origin + new Vector2(PillPaddingX, 3f), textAbgr, label); + + // Reserve the layout slot so SameLine after the pill knows the width. + ImGui.Dummy(new Vector2(width, PillHeight)); + } + + private void DrawInputField() + { + ImGui.SetNextItemWidth(-90f); + ImGui.InputText("##hellion-input", ref _pendingMessage, BufferCapacity); + } + + private void DrawQuickButtons() + { + using (_fonts.FontAwesome.Push()) + { + if (ImGui.Button(FontAwesomeIcon.SmileBeam.ToIconString())) + _symbolPicker.OpenPopup(); + if (ImGui.IsItemHovered()) + { + using (ImRaii.DefaultFont()) + ImGui.SetTooltip("Insert symbol"); + } + + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString())) + { + // Settings toggle wires up when the plugin window registers + // its open handler; no-op until then so the button is + // visible without dragging a half-finished settings call + // into the component. + } + if (ImGui.IsItemHovered()) + { + using (ImRaii.DefaultFont()) + ImGui.SetTooltip("Settings"); + } + + ImGui.SameLine(); + var hidden = Plugin.Config.HideChat; + var visIcon = hidden ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; + if (ImGui.Button(visIcon.ToIconString())) + Plugin.Config.HideChat = !hidden; + if (ImGui.IsItemHovered()) + { + using (ImRaii.DefaultFont()) + ImGui.SetTooltip(hidden ? "Unhide chat" : "Hide chat"); + } + } + } +}