feat(ui): add InputBar with channel pill and symbol picker overlay

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.
This commit is contained in:
2026-05-23 19:17:25 +02:00
parent bdfb1298ad
commit 36afce21e5
2 changed files with 151 additions and 0 deletions
+145
View File
@@ -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");
}
}
}
}