feat(chat): the input row learns the window's own language

Five filled plates become two ghost buttons and a menu. The plates were ImGui
defaults sitting between a drawn pill and a drawn status bar -- three shape
languages in the one row a user works in, which is what the tester's screenshot
made obvious. The ghosts follow the sidebar's icon buttons: nothing at rest, a
held hover fill, the glyph lifting toward the accent.

Symbols stay outside the menu because they are used mid-sentence. Theme,
settings, screenshot and hide move in; the screenshot toggle was allowed in only
because its state moved to the status bar first, as an accent pill that shows
while the mode is on. A privacy state behind a closed menu answers nothing.

The input field paints its own rounded surface and hands ImGui a transparent
frame, so the widget draws only text and caret -- the Boutique.Inputs technique
from Character Select+, no rebuild of the widget itself. Focus is a two-pixel
accent rail on the left edge: the same mark the rows, the popups and the message
list already use for "this is where you are".

The channel pill gets the CS+ treatment, a faint white gradient and a one-pixel
light along the top edge. White over the fill rather than a second hue, so it
reads as depth in every palette.

And the popups that open from the row are made of PopupRow now -- the sidebar
row's shape, which is also exactly what CS+ draws for its own popup entries. The
channel picker was the reported case: a drawn pill opening a list of naked
selectables, the style breaking mid-click.

A hundred-odd pixels of button reserve go back to the input field.
This commit is contained in:
2026-08-19 18:30:15 +02:00
parent df6296024b
commit 6bdfecb1df
30 changed files with 418 additions and 94 deletions
+1
View File
@@ -389,6 +389,7 @@ internal class HellionStrings
internal static string Settings_Preview_TitleMock => Get(nameof(Settings_Preview_TitleMock));
internal static string Settings_Preview_StatusOpen => Get(nameof(Settings_Preview_StatusOpen));
internal static string ChannelHeader_NotLoggedIn => Get(nameof(ChannelHeader_NotLoggedIn));
internal static string InputBar_More_Tooltip => Get(nameof(InputBar_More_Tooltip));
internal static string Settings_Section_Links => Get(nameof(Settings_Section_Links));
internal static string Settings_Section_Behaviour => Get(nameof(Settings_Section_Behaviour));
internal static string Settings_Section_Keybinds => Get(nameof(Settings_Section_Keybinds));
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Sessió no iniciada</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Més accions</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Nepřihlášen</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Další akce</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Ikke logget ind</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Flere handlinger</value>
</data>
</root>
@@ -1355,4 +1355,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Nicht eingeloggt</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Weitere Aktionen</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Εκτός σύνδεσης</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Περισσότερες ενέργειες</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Sesión no iniciada</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Más acciones</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Ei kirjautuneena</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Lisää toimintoja</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Non connecté</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Plus d'actions</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Nincs bejelentkezve</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>További műveletek</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Non connesso</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Altre azioni</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>未ログイン</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>その他の操作</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>로그인되지 않음</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>추가 작업</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Ikke innlogget</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Flere handlinger</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Niet ingelogd</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Meer acties</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Niezalogowany</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Więcej działań</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Não conectado</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Mais ações</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Sem sessão iniciada</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Mais ações</value>
</data>
</root>
@@ -1372,4 +1372,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Not logged in</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>More actions</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Neconectat</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Mai multe acțiuni</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Вход не выполнен</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Другие действия</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Inte inloggad</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Fler åtgärder</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Oturum açılmadı</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Diğer işlemler</value>
</data>
</root>
@@ -1360,4 +1360,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>Вхід не виконано</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>Інші дії</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>未登录</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>更多操作</value>
</data>
</root>
@@ -1361,4 +1361,7 @@
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
<value>未登入</value>
</data>
<data name="InputBar_More_Tooltip" xml:space="preserve">
<value>更多操作</value>
</data>
</root>
+220 -90
View File
@@ -273,6 +273,18 @@ internal sealed class InputBar
StyleEngine.Widgets.Pill.Draw(origin, label, pillAbgr, textAbgr);
// The CS+ pill treatment: a faint white gradient from the top and a
// one-pixel light along the upper edge. Reads as depth in every palette
// because it is white over the fill, not a second hue.
var pillDl = ImGui.GetWindowDrawList();
var pillScale = StyleEngine.Metrics.Scale;
pillDl.DrawVerticalGradient(origin, origin + size, 0x28FFFFFFu, 0u);
pillDl.AddRectFilled(
new Vector2(origin.X + 6f * pillScale, origin.Y),
new Vector2(origin.X + size.X - 6f * pillScale, origin.Y + 1f * pillScale),
0x50FFFFFFu
);
// Hit area over the rendered pill so a click opens the channel
// picker. InvisibleButton both reserves the layout slot and gives
// the popup a stable anchor item.
@@ -283,7 +295,7 @@ internal sealed class InputBar
DrawChannelPickerPopup(tab);
}
private static void DrawChannelPickerPopup(Tab? tab)
private void DrawChannelPickerPopup(Tab? tab)
{
if (!ImGui.BeginPopup("##hellion-channel-picker"))
return;
@@ -296,14 +308,27 @@ internal sealed class InputBar
return;
}
ImGui.Dummy(new Vector2(150f * StyleEngine.Metrics.Scale, 0f));
var i = 0;
foreach (var chatType in tab.SelectedChannels.Keys)
{
if (chatType.ToInputChannel() is not { } input)
continue;
var isCurrent = tab.CurrentChannel.Channel == input;
if (ImGui.Selectable(input.ToChatType().Name(), isCurrent))
if (
StyleEngine.Widgets.PopupRow.Draw(
$"##ch-{i++}",
input.ToChatType().Name(),
isCurrent,
_fonts
)
)
{
tab.CurrentChannel.SetChannel(input);
ImGui.CloseCurrentPopup();
}
}
}
finally
@@ -325,6 +350,33 @@ internal sealed class InputBar
Activate = false;
}
// Boutique.Inputs technique: paint the surface, then hand ImGui a
// transparent frame so the widget draws only text and caret. The focus
// rail is the same two-pixel accent bar the rows and popups carry --
// one vocabulary for "this is where you are".
var scale = StyleEngine.Metrics.Scale;
var fieldOrigin = ImGui.GetCursorScreenPos();
var fieldSize = new Vector2(
ImGui.GetContentRegionAvail().X - QuickButtonsReserve,
ImGui.GetFrameHeight()
);
ImGui
.GetWindowDrawList()
.AddRectFilled(
fieldOrigin,
fieldOrigin + fieldSize,
ColourUtil.RgbaToAbgr(_themes.Active.Colors.FrameBg),
6f * scale
);
using var frameBg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero);
using var frameBgHovered = ImRaii.PushColor(ImGuiCol.FrameBgHovered, Vector4.Zero);
using var frameBgActive = ImRaii.PushColor(ImGuiCol.FrameBgActive, Vector4.Zero);
using var framePad = ImRaii.PushStyle(
ImGuiStyleVar.FramePadding,
new Vector2(10f * scale, ImGui.GetStyle().FramePadding.Y)
);
ImGui.SetNextItemWidth(-QuickButtonsReserve);
if (
ImGui.InputText(
@@ -343,6 +395,15 @@ internal sealed class InputBar
_commandHelpWindow.IsOpen = false;
TrySend(activeTab);
}
if (ImGui.IsItemActive())
ImGui
.GetWindowDrawList()
.AddRectFilled(
fieldOrigin,
new Vector2(fieldOrigin.X + 2f * scale, fieldOrigin.Y + fieldSize.Y),
ColourUtil.RgbaToAbgr(_themes.Active.Colors.Accent)
);
DrawInputContextMenu();
_isFocused = ImGui.IsItemFocused();
@@ -627,106 +688,175 @@ internal sealed class InputBar
return (current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}", false);
}
// Two ghost buttons and a menu, where five filled plates used to sit. The
// plates were ImGui defaults in a row whose pill and status bar are drawn;
// the ghosts follow the sidebar's icon buttons instead -- nothing at rest, a
// held hover fill, the glyph lifting toward the accent.
//
// Symbols stay outside the menu because they are used mid-sentence; a click
// through a drawer for that would be a regression. The screenshot toggle may
// live in the menu only because its state moved to the status bar first --
// the whole point of that mode is knowing what your screen shows BEFORE the
// screenshot key, and a state behind a closed menu answers nothing.
private void DrawQuickButtons()
{
// Collected here and drawn after the icon font is popped. Inside the push
// it rendered against the FontAwesome atlas, which has no ASCII glyphs, so
// every tooltip came out as an empty box.
string? tooltip = null;
using (_fonts.FontAwesome.Push())
{
if (ImGui.Button(FontAwesomeIcon.SmileBeam.ToIconString()))
_symbolPicker.OpenPopup();
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip;
if (DrawGhostButton("##qb-symbols", FontAwesomeIcon.SmileBeam))
_symbolPicker.OpenPopup();
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip;
if (_themeQuickPicker is not null)
{
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.Palette.ToIconString()))
_themeQuickPicker.OpenPopup();
if (ImGui.IsItemHovered())
tooltip = HellionStrings.Settings_QuickPicker_Tooltip;
}
ImGui.SameLine(0f, 4f * StyleEngine.Metrics.Scale);
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString()))
{
_onOpenSettings();
}
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_Settings_Tooltip;
if (DrawGhostButton("##qb-more", FontAwesomeIcon.EllipsisH))
ImGui.OpenPopup("##hellion-more-menu");
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_More_Tooltip;
// Screenshot mode. It lived only in the right-click menu on a player
// name, which for a privacy feature is the same as not existing --
// reported as missing by a tester who had been using the plugin for
// months. Lit when active, because a mode you cannot see the state of
// is worse than no mode.
ImGui.SameLine();
var shooting = Plugin.Config.ScreenshotMode;
using (
ImRaii.PushColor(
ImGuiCol.Text,
ColourUtil.RgbaToVector4(
_resolver.Resolve(Token.AccentEmber, _themes.Active.Colors)
),
shooting
)
)
{
if (ImGui.Button(FontAwesomeIcon.Camera.ToIconString()))
Plugin.Config.ScreenshotMode = !Plugin.Config.ScreenshotMode;
}
if (ImGui.IsItemHovered())
tooltip = Language.Context_ScreenshotMode;
// Hides the window (1.5.6 UserHide). One-way — Enter brings it back.
// Main window only; last in the row there.
if (Plugin.Config.ShowHideButton && _onHideWindow is not null)
{
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.EyeSlash.ToIconString()))
_onHideWindow();
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_HideChat_Tooltip;
}
// Pop-in, in the pop-out windows only. It sits here rather than in a
// header row because a pop-out with its title bar on had no header at
// all -- and the title bar carries no close button, since closing has
// to go through the pool to release the slot.
if (OnPopIn is not null)
{
ImGui.SameLine();
// Red, and contrast-checked against the button plate it sits on
// rather than taken raw: several themes ship a danger colour that
// is nearly invisible on their own button fill.
var danger = ColourUtil.RgbaToAbgr(
_resolver.Resolve(Token.StatusDanger, _themes.Active.Colors)
);
var plate = ColourUtil.Vector4ToAbgr(ImGui.GetStyle().Colors[(int)ImGuiCol.Button]);
using (
ImRaii.PushColor(
ImGuiCol.Text,
ColourUtil.RgbaToVector4(
ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(danger, plate, 3f))
)
)
)
{
if (ImGui.Button(FontAwesomeIcon.Times.ToIconString()))
OnPopIn();
}
if (ImGui.IsItemHovered())
tooltip = HellionStrings.InputBar_PopIn_Tooltip;
}
}
DrawMoreMenu();
if (tooltip is not null)
ImGui.SetTooltip(tooltip);
}
// Sidebar language: no plate at rest, held hover fill, glyph toward accent.
private bool DrawGhostButton(string id, FontAwesomeIcon icon, bool lit = false)
{
var scale = StyleEngine.Metrics.Scale;
var side = MathF.Round(24f * scale);
var origin = ImGui.GetCursorScreenPos();
var max = origin + new Vector2(side, side);
var clicked = ImGui.InvisibleButton(id, new Vector2(side, side));
var hovered = ImGui.IsItemHovered();
var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), hovered);
var c = _themes.Active.Colors;
var dl = ImGui.GetWindowDrawList();
if (amount > 0f)
dl.AddRectFilled(
origin,
max,
ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount),
3f * scale
);
var accent = ColourUtil.RgbaToAbgr(c.Accent);
var tint = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.ChildBg, 4.5f));
if (lit)
tint = accent;
else if (amount > 0f)
tint = ColourUtil.Lerp(tint, accent, amount * 0.6f);
using (_fonts.FontAwesome.Push())
{
var glyph = icon.ToIconString();
var size = ImGui.CalcTextSize(glyph);
dl.AddText(origin + (max - origin - size) * 0.5f, tint, glyph);
}
return clicked;
}
private void DrawMoreMenu()
{
if (!ImGui.BeginPopup("##hellion-more-menu"))
return;
// Actions run after EndPopup: the theme picker opens its own popup at
// this component's ID scope, not inside the menu's.
var openTheme = false;
try
{
ImGui.Dummy(new Vector2(160f * StyleEngine.Metrics.Scale, 0f));
if (
_themeQuickPicker is not null
&& StyleEngine.Widgets.PopupRow.Draw(
"##mm-theme",
HellionStrings.Settings_QuickPicker_Tooltip,
active: false,
_fonts,
FontAwesomeIcon.Palette
)
)
{
openTheme = true;
ImGui.CloseCurrentPopup();
}
if (
StyleEngine.Widgets.PopupRow.Draw(
"##mm-settings",
HellionStrings.InputBar_Settings_Tooltip,
active: false,
_fonts,
FontAwesomeIcon.Cog
)
)
{
_onOpenSettings();
ImGui.CloseCurrentPopup();
}
// Shows its state right here as the active row, and again in the
// status bar once on -- the menu closes, the awareness must not.
if (
StyleEngine.Widgets.PopupRow.Draw(
"##mm-screenshot",
Language.Context_ScreenshotMode,
active: Plugin.Config.ScreenshotMode,
_fonts,
FontAwesomeIcon.Camera
)
)
{
Plugin.Config.ScreenshotMode = !Plugin.Config.ScreenshotMode;
}
if (
Plugin.Config.ShowHideButton
&& _onHideWindow is not null
&& StyleEngine.Widgets.PopupRow.Draw(
"##mm-hide",
HellionStrings.InputBar_HideChat_Tooltip,
active: false,
_fonts,
FontAwesomeIcon.EyeSlash
)
)
{
_onHideWindow();
ImGui.CloseCurrentPopup();
}
if (
OnPopIn is not null
&& StyleEngine.Widgets.PopupRow.Draw(
"##mm-popin",
HellionStrings.InputBar_PopIn_Tooltip,
active: false,
_fonts,
FontAwesomeIcon.Times
)
)
{
OnPopIn();
ImGui.CloseCurrentPopup();
}
}
finally
{
ImGui.EndPopup();
}
if (openTheme)
_themeQuickPicker!.OpenPopup();
}
// Test-only hook; do not call from production code.
internal void TestSetPendingMessageForSelfTest(string value) => _pendingMessage = value;
+24
View File
@@ -203,6 +203,30 @@ internal sealed class StatusBar
FontAwesomeIcon.Lock,
_fonts.FontAwesome
);
// Screenshot mode announces itself here while it is on. The toggle sits
// in the input row's menu, and a closed menu answers nothing -- the whole
// point of the mode is knowing what the screen shows before the
// screenshot key is pressed. Accent text, because this one is a state
// the user chose, not ambient information.
if (Plugin.Config.ScreenshotMode)
{
var camWidth = StyleEngine.Widgets.Pill.MeasureIcon(
FontAwesomeIcon.Camera,
_fonts.FontAwesome
);
var camLabel = Language.Context_ScreenshotMode;
if (Fits(camLabel, withDot: false, camWidth))
x +=
DrawSlot(
new Vector2(x, top),
camLabel,
pillFill,
palette.Abgr(Token.AccentPrimary, colors),
null,
(FontAwesomeIcon.Camera, _fonts.FontAwesome)
) + gap;
}
if (Fits(privacyLabel, withDot: false, lockWidth))
x +=
DrawSlot(
+4 -4
View File
@@ -27,10 +27,10 @@ internal static class Metrics
// --- Input bar ---
internal const float InputBarHeightRaw = 32f;
// Raised from 130 in v1.13.0 for the screenshot-mode button. Five buttons in
// the main window (symbols, theme, screenshot, settings, hide) and five in a
// pop-out, where pop-in takes the place of hide.
internal const float InputQuickButtonsReserveRaw = 156f;
// Two ghost buttons since v1.14.0 -- symbols and the more-menu. Everything
// else moved into the menu, and the hundred-odd pixels went back to the
// input field.
internal const float InputQuickButtonsReserveRaw = 62f;
// --- Honorific header ---
internal const float HonorificHeightRaw = 30f;
@@ -0,0 +1,94 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using HellionChat.Util;
namespace HellionChat.Ui.StyleEngine.Widgets;
// The row a popup is made of. Replaces ImGui.Selectable inside the plugin's
// popups, which carried the right colours through GlobalStyleScope but none of
// the shape the rest of the window speaks: no held hover, no accent bar on the
// active entry.
//
// The shape is the sidebar row's, and it is also what Character Select+ draws
// for its popup entries (Boutique.Dropdowns, DrawPopupRow): a soft fill on
// hover, a two-pixel accent bar when active. One conversation, wherever a list
// appears.
internal static class PopupRow
{
private const float PadXRaw = 10f;
private const float PadYRaw = 5f;
private const float IconGapRaw = 8f;
private const float AccentBarRaw = 2f;
internal static bool Draw(
string id,
string label,
bool active,
FontManager fonts,
FontAwesomeIcon? icon = null
)
{
var scale = Metrics.Scale;
var theme = Plugin.Instance.ThemeRegistry.Active;
var c = theme.Colors;
var height = ImGui.GetTextLineHeight() + MathF.Round(PadYRaw * 2f * scale);
var width = ImGui.GetContentRegionAvail().X;
var origin = ImGui.GetCursorScreenPos();
var max = origin + new Vector2(width, height);
var clicked = ImGui.InvisibleButton(id, new Vector2(width, height));
var hovered = ImGui.IsItemHovered();
var amount = HoverState.Query(ImGui.GetID(id), hovered);
var dl = ImGui.GetWindowDrawList();
var accentAbgr = ColourUtil.RgbaToAbgr(c.Accent);
// Active first, hover on top -- an active entry still answers the mouse.
if (active)
dl.AddRectFilled(origin, max, ColourUtil.ApplyAlpha(accentAbgr, 0.10f), 3f * scale);
if (amount > 0f)
dl.AddRectFilled(
origin,
max,
ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount),
3f * scale
);
if (active)
dl.AddRectFilled(
origin,
new Vector2(origin.X + AccentBarRaw * scale, max.Y),
accentAbgr
);
var textAbgr = ColourUtil.RgbaToAbgr(
ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f)
);
if (active || amount > 0f)
textAbgr = ColourUtil.Lerp(textAbgr, accentAbgr, active ? 0.6f : amount * 0.4f);
var x = origin.X + PadXRaw * scale;
if (icon is { } glyph)
{
using (fonts.FontAwesome.Push())
{
var s = glyph.ToIconString();
var size = ImGui.CalcTextSize(s);
dl.AddText(new Vector2(x, origin.Y + (height - size.Y) * 0.5f), textAbgr, s);
x += size.X + IconGapRaw * scale;
}
}
dl.AddText(
new Vector2(x, origin.Y + (height - ImGui.GetTextLineHeight()) * 0.5f),
textAbgr,
label
);
return clicked;
}
}