Merge branch 'feature/v1.14.0' into main
The input row cycle: two ghost buttons and a menu where five filled defaults sat, a field with its own surface and focus rail, popups built from the sidebar's row shape, a softened seam to the conversation, and hover fades at a human pace. Plus the style lab and the preflight guard for the RGBA/ABGR trap. Local state only. repo.json stays on 1.5.6.0. Note on shape: these commits were made directly on main by mistake and were re-homed onto feature/v1.14.0 before this merge, so the branch-per-cycle rule holds in the history even where it slipped in the moment.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.13.0</Version>
|
<Version>1.14.0</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
internal Ui.Windows.WidgetGalleryWindow WidgetGallery { get; private set; } = null!;
|
internal Ui.Windows.WidgetGalleryWindow WidgetGallery { get; private set; } = null!;
|
||||||
#endif
|
#endif
|
||||||
|
internal Ui.Windows.InputBarLabWindow InputBarLab { get; private set; } = null!;
|
||||||
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
|
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
|
||||||
internal DebuggerWindow DebuggerWindow { get; private set; } = null!;
|
internal DebuggerWindow DebuggerWindow { get; private set; } = null!;
|
||||||
|
|
||||||
@@ -456,6 +457,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
WidgetGallery = _host.Services.GetRequiredService<Ui.Windows.WidgetGalleryWindow>();
|
WidgetGallery = _host.Services.GetRequiredService<Ui.Windows.WidgetGalleryWindow>();
|
||||||
#endif
|
#endif
|
||||||
|
InputBarLab = _host.Services.GetRequiredService<Ui.Windows.InputBarLabWindow>();
|
||||||
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
|
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
|
||||||
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
|
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
|
||||||
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
|
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
|
||||||
@@ -1042,6 +1044,14 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
WidgetGallery.Toggle();
|
WidgetGallery.Toggle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (arg.Equals("lab", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
InputBarLab.Toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
#endif
|
#endif
|
||||||
if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase))
|
if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||||
));
|
));
|
||||||
#endif
|
#endif
|
||||||
|
// The style lab: variants side by side, in-game, against the live theme.
|
||||||
|
// Permanent by Flo's call, and deliberately not behind DEBUG -- style
|
||||||
|
// decisions happen in the build he actually runs.
|
||||||
|
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
||||||
|
sp.GetRequiredService<Plugin>()
|
||||||
|
));
|
||||||
services.AddSingleton(sp => new DebuggerWindow(
|
services.AddSingleton(sp => new DebuggerWindow(
|
||||||
sp.GetRequiredService<Plugin>(),
|
sp.GetRequiredService<Plugin>(),
|
||||||
sp.GetRequiredService<PayloadHandler>()
|
sp.GetRequiredService<PayloadHandler>()
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ internal sealed class PluginLifecycle : IAsyncDisposable
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
plugin.WindowSystem.AddWindow(plugin.WidgetGallery);
|
plugin.WindowSystem.AddWindow(plugin.WidgetGallery);
|
||||||
#endif
|
#endif
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.InputBarLab);
|
||||||
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
||||||
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
||||||
|
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Preview_TitleMock => Get(nameof(Settings_Preview_TitleMock));
|
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 Settings_Preview_StatusOpen => Get(nameof(Settings_Preview_StatusOpen));
|
||||||
internal static string ChannelHeader_NotLoggedIn => Get(nameof(ChannelHeader_NotLoggedIn));
|
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_Links => Get(nameof(Settings_Section_Links));
|
||||||
internal static string Settings_Section_Behaviour => Get(nameof(Settings_Section_Behaviour));
|
internal static string Settings_Section_Behaviour => Get(nameof(Settings_Section_Behaviour));
|
||||||
internal static string Settings_Section_Keybinds => Get(nameof(Settings_Section_Keybinds));
|
internal static string Settings_Section_Keybinds => Get(nameof(Settings_Section_Keybinds));
|
||||||
|
|||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Sessió no iniciada</value>
|
<value>Sessió no iniciada</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Més accions</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Nepřihlášen</value>
|
<value>Nepřihlášen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Další akce</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Ikke logget ind</value>
|
<value>Ikke logget ind</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Flere handlinger</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1355,4 +1355,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Nicht eingeloggt</value>
|
<value>Nicht eingeloggt</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Weitere Aktionen</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Εκτός σύνδεσης</value>
|
<value>Εκτός σύνδεσης</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Περισσότερες ενέργειες</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Sesión no iniciada</value>
|
<value>Sesión no iniciada</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Más acciones</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Ei kirjautuneena</value>
|
<value>Ei kirjautuneena</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Lisää toimintoja</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Non connecté</value>
|
<value>Non connecté</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Plus d'actions</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Nincs bejelentkezve</value>
|
<value>Nincs bejelentkezve</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>További műveletek</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Non connesso</value>
|
<value>Non connesso</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Altre azioni</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>未ログイン</value>
|
<value>未ログイン</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>その他の操作</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>로그인되지 않음</value>
|
<value>로그인되지 않음</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>추가 작업</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Ikke innlogget</value>
|
<value>Ikke innlogget</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Flere handlinger</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Niet ingelogd</value>
|
<value>Niet ingelogd</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Meer acties</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Niezalogowany</value>
|
<value>Niezalogowany</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Więcej działań</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Não conectado</value>
|
<value>Não conectado</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Mais ações</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Sem sessão iniciada</value>
|
<value>Sem sessão iniciada</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Mais ações</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1372,4 +1372,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Not logged in</value>
|
<value>Not logged in</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>More actions</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Neconectat</value>
|
<value>Neconectat</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Mai multe acțiuni</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Вход не выполнен</value>
|
<value>Вход не выполнен</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Другие действия</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Inte inloggad</value>
|
<value>Inte inloggad</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Fler åtgärder</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Oturum açılmadı</value>
|
<value>Oturum açılmadı</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Diğer işlemler</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1360,4 +1360,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>Вхід не виконано</value>
|
<value>Вхід не виконано</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>Інші дії</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>未登录</value>
|
<value>未登录</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>更多操作</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1361,4 +1361,7 @@
|
|||||||
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
<data name="ChannelHeader_NotLoggedIn" xml:space="preserve">
|
||||||
<value>未登入</value>
|
<value>未登入</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="InputBar_More_Tooltip" xml:space="preserve">
|
||||||
|
<value>更多操作</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -182,7 +182,13 @@ internal sealed class InputBar
|
|||||||
var pillToken = isTell ? Token.AccentEmber : Token.AccentPrimary;
|
var pillToken = isTell ? Token.AccentEmber : Token.AccentPrimary;
|
||||||
var pillRgba = _resolver.Resolve(pillToken, theme.Colors);
|
var pillRgba = _resolver.Resolve(pillToken, theme.Colors);
|
||||||
var pillAbgr = ColourUtil.RgbaToAbgr(pillRgba);
|
var pillAbgr = ColourUtil.RgbaToAbgr(pillRgba);
|
||||||
var pillTextAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
// Measured against the pill fill it lands on, not taken from the theme
|
||||||
|
// raw -- an accent fill can swallow the theme's text colour whole.
|
||||||
|
var pillTextAbgr = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary),
|
||||||
|
pillAbgr,
|
||||||
|
4.5f
|
||||||
|
);
|
||||||
|
|
||||||
DrawChannelPill(activeTab, isTell, pillAbgr, pillTextAbgr);
|
DrawChannelPill(activeTab, isTell, pillAbgr, pillTextAbgr);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -271,7 +277,18 @@ internal sealed class InputBar
|
|||||||
var size = StyleEngine.Widgets.Pill.CalcSize(label, withDot: false);
|
var size = StyleEngine.Widgets.Pill.CalcSize(label, withDot: false);
|
||||||
var origin = ImGui.GetCursorScreenPos();
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
|
||||||
StyleEngine.Widgets.Pill.Draw(origin, label, pillAbgr, textAbgr);
|
// Chamfered, not rounded: the shape the segmented control already uses
|
||||||
|
// for its selected segment, with the CS+ white gradient for depth. The
|
||||||
|
// slip corner is the one piece of the Boutique geometry the plugin had
|
||||||
|
// built and barely used.
|
||||||
|
var pillDl = ImGui.GetWindowDrawList();
|
||||||
|
var pillScale = StyleEngine.Metrics.Scale;
|
||||||
|
var pillMax = origin + size;
|
||||||
|
pillDl.DrawSlipPolygon(origin, pillMax, ColourUtil.RgbaToAbgr(pillAbgr), 6f * pillScale);
|
||||||
|
pillDl.DrawVerticalGradient(origin, pillMax, 0x28FFFFFFu, 0u);
|
||||||
|
|
||||||
|
var labelSize = ImGui.CalcTextSize(label);
|
||||||
|
pillDl.AddText(origin + (size - labelSize) * 0.5f, textAbgr, label);
|
||||||
|
|
||||||
// Hit area over the rendered pill so a click opens the channel
|
// Hit area over the rendered pill so a click opens the channel
|
||||||
// picker. InvisibleButton both reserves the layout slot and gives
|
// picker. InvisibleButton both reserves the layout slot and gives
|
||||||
@@ -283,7 +300,7 @@ internal sealed class InputBar
|
|||||||
DrawChannelPickerPopup(tab);
|
DrawChannelPickerPopup(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawChannelPickerPopup(Tab? tab)
|
private void DrawChannelPickerPopup(Tab? tab)
|
||||||
{
|
{
|
||||||
if (!ImGui.BeginPopup("##hellion-channel-picker"))
|
if (!ImGui.BeginPopup("##hellion-channel-picker"))
|
||||||
return;
|
return;
|
||||||
@@ -296,14 +313,38 @@ internal sealed class InputBar
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pickerWidth = 0f;
|
||||||
|
foreach (var chatType in tab.SelectedChannels.Keys)
|
||||||
|
{
|
||||||
|
if (chatType.ToInputChannel() is not { } ch)
|
||||||
|
continue;
|
||||||
|
pickerWidth = MathF.Max(
|
||||||
|
pickerWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(ch.ToChatType().Name(), null, _fonts)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(pickerWidth, 0f));
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
foreach (var chatType in tab.SelectedChannels.Keys)
|
foreach (var chatType in tab.SelectedChannels.Keys)
|
||||||
{
|
{
|
||||||
if (chatType.ToInputChannel() is not { } input)
|
if (chatType.ToInputChannel() is not { } input)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var isCurrent = tab.CurrentChannel.Channel == input;
|
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);
|
tab.CurrentChannel.SetChannel(input);
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -325,6 +366,33 @@ internal sealed class InputBar
|
|||||||
Activate = false;
|
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);
|
ImGui.SetNextItemWidth(-QuickButtonsReserve);
|
||||||
if (
|
if (
|
||||||
ImGui.InputText(
|
ImGui.InputText(
|
||||||
@@ -343,6 +411,15 @@ internal sealed class InputBar
|
|||||||
_commandHelpWindow.IsOpen = false;
|
_commandHelpWindow.IsOpen = false;
|
||||||
TrySend(activeTab);
|
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();
|
DrawInputContextMenu();
|
||||||
|
|
||||||
_isFocused = ImGui.IsItemFocused();
|
_isFocused = ImGui.IsItemFocused();
|
||||||
@@ -627,106 +704,246 @@ internal sealed class InputBar
|
|||||||
return (current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}", false);
|
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()
|
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;
|
string? tooltip = null;
|
||||||
|
|
||||||
using (_fonts.FontAwesome.Push())
|
if (DrawGhostButton("##qb-symbols", FontAwesomeIcon.SmileBeam))
|
||||||
{
|
_symbolPicker.OpenPopup();
|
||||||
if (ImGui.Button(FontAwesomeIcon.SmileBeam.ToIconString()))
|
if (ImGui.IsItemHovered())
|
||||||
_symbolPicker.OpenPopup();
|
tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip;
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip;
|
|
||||||
|
|
||||||
if (_themeQuickPicker is not null)
|
ImGui.SameLine(0f, 4f * StyleEngine.Metrics.Scale);
|
||||||
{
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button(FontAwesomeIcon.Palette.ToIconString()))
|
|
||||||
_themeQuickPicker.OpenPopup();
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
tooltip = HellionStrings.Settings_QuickPicker_Tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
if (DrawGhostButton("##qb-more", FontAwesomeIcon.EllipsisH))
|
||||||
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString()))
|
ImGui.OpenPopup("##hellion-more-menu");
|
||||||
{
|
if (ImGui.IsItemHovered())
|
||||||
_onOpenSettings();
|
tooltip = HellionStrings.InputBar_More_Tooltip;
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
tooltip = HellionStrings.InputBar_Settings_Tooltip;
|
|
||||||
|
|
||||||
// Screenshot mode. It lived only in the right-click menu on a player
|
DrawMoreMenu();
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tooltip is not null)
|
if (tooltip is not null)
|
||||||
ImGui.SetTooltip(tooltip);
|
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();
|
||||||
|
|
||||||
|
// Every colour here is measured against what it actually lands on. The
|
||||||
|
// glyph sits on the window floor, the glow hugs the fill -- a raw theme
|
||||||
|
// accent can vanish on either, which is exactly the warning that came
|
||||||
|
// back from the smoke test.
|
||||||
|
var accent = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent),
|
||||||
|
ColourUtil.RgbaToAbgr(c.ChildBg),
|
||||||
|
3f
|
||||||
|
);
|
||||||
|
|
||||||
|
if (amount > 0f)
|
||||||
|
{
|
||||||
|
dl.AddRectFilled(
|
||||||
|
origin,
|
||||||
|
max,
|
||||||
|
ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.Surface), amount * 0.8f),
|
||||||
|
3f * scale
|
||||||
|
);
|
||||||
|
|
||||||
|
// DrawGlowBorder reads RGBA with alpha in the low byte; ApplyAlpha
|
||||||
|
// writes the high byte (ABGR). Mixing them is the channel-order trap
|
||||||
|
// this cycle already fell into once, so the alpha byte is set by hand.
|
||||||
|
var glowRgba =
|
||||||
|
(ColourUtil.RgbaToAbgr(accent) & 0xFFFFFF00u)
|
||||||
|
| (uint)(byte)Math.Round(0xB4 * amount);
|
||||||
|
dl.DrawGlowBorder(min: origin, max: max, glowRgba, 1f, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tint = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.TextMuted),
|
||||||
|
ColourUtil.RgbaToAbgr(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
|
||||||
|
{
|
||||||
|
// Width from the widest visible entry, not a fixed minimum -- the
|
||||||
|
// popup does not grow for draw-list content, and German labels
|
||||||
|
// outran the first guess within a day.
|
||||||
|
var menuWidth = 0f;
|
||||||
|
if (_themeQuickPicker is not null)
|
||||||
|
menuWidth = MathF.Max(
|
||||||
|
menuWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(
|
||||||
|
HellionStrings.Settings_QuickPicker_Tooltip,
|
||||||
|
FontAwesomeIcon.Palette,
|
||||||
|
_fonts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
menuWidth = MathF.Max(
|
||||||
|
menuWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(
|
||||||
|
HellionStrings.InputBar_Settings_Tooltip,
|
||||||
|
FontAwesomeIcon.Cog,
|
||||||
|
_fonts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
menuWidth = MathF.Max(
|
||||||
|
menuWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(
|
||||||
|
Language.Context_ScreenshotMode,
|
||||||
|
FontAwesomeIcon.Camera,
|
||||||
|
_fonts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (Plugin.Config.ShowHideButton && _onHideWindow is not null)
|
||||||
|
menuWidth = MathF.Max(
|
||||||
|
menuWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(
|
||||||
|
HellionStrings.InputBar_HideChat_Tooltip,
|
||||||
|
FontAwesomeIcon.EyeSlash,
|
||||||
|
_fonts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (OnPopIn is not null)
|
||||||
|
menuWidth = MathF.Max(
|
||||||
|
menuWidth,
|
||||||
|
StyleEngine.Widgets.PopupRow.CalcWidth(
|
||||||
|
HellionStrings.InputBar_PopIn_Tooltip,
|
||||||
|
FontAwesomeIcon.Times,
|
||||||
|
_fonts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(menuWidth, 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.
|
// Test-only hook; do not call from production code.
|
||||||
internal void TestSetPendingMessageForSelfTest(string value) => _pendingMessage = value;
|
internal void TestSetPendingMessageForSelfTest(string value) => _pendingMessage = value;
|
||||||
|
|
||||||
|
|||||||
@@ -269,20 +269,10 @@ internal sealed class LivePreviewPanel : IDisposable
|
|||||||
|
|
||||||
draw.AddRectFilled(listOrigin, max, ColourUtil.RgbaToAbgr(theme.Colors.WindowBg));
|
draw.AddRectFilled(listOrigin, max, ColourUtil.RgbaToAbgr(theme.Colors.WindowBg));
|
||||||
|
|
||||||
// The channel header the real window now carries above its log. Tracked
|
// The channel header the real window carries above its log: tracked caps
|
||||||
// caps, same as the widget draws them.
|
// and a rule that fades out, no plate. Same reasoning as the section
|
||||||
|
// headings in this very window.
|
||||||
var headerHeight = ImGui.GetTextLineHeight() + 8f;
|
var headerHeight = ImGui.GetTextLineHeight() + 8f;
|
||||||
draw.AddRectFilled(
|
|
||||||
listOrigin,
|
|
||||||
new Vector2(max.X, listOrigin.Y + headerHeight),
|
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Surface)
|
|
||||||
);
|
|
||||||
draw.AddLine(
|
|
||||||
new Vector2(listOrigin.X, listOrigin.Y + headerHeight),
|
|
||||||
new Vector2(max.X, listOrigin.Y + headerHeight),
|
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Border),
|
|
||||||
1f
|
|
||||||
);
|
|
||||||
draw.DrawTrackedText(
|
draw.DrawTrackedText(
|
||||||
new Vector2(listOrigin.X + 6f, listOrigin.Y + 4f),
|
new Vector2(listOrigin.X + 6f, listOrigin.Y + 4f),
|
||||||
MockChannel,
|
MockChannel,
|
||||||
@@ -295,6 +285,16 @@ internal sealed class LivePreviewPanel : IDisposable
|
|||||||
1.8f
|
1.8f
|
||||||
);
|
);
|
||||||
|
|
||||||
|
draw.DrawFadeRule(
|
||||||
|
new Vector2(
|
||||||
|
listOrigin.X + 6f + 68f,
|
||||||
|
listOrigin.Y + 4f + ImGui.GetTextLineHeight() * 0.5f
|
||||||
|
),
|
||||||
|
listWidth - 6f - 68f - 6f,
|
||||||
|
ColourUtil.RgbaToAbgr(theme.Colors.Border),
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
|
||||||
var padMin = new Vector2(listOrigin.X + 2f, max.Y - 6f);
|
var padMin = new Vector2(listOrigin.X + 2f, max.Y - 6f);
|
||||||
draw.AddRectFilled(
|
draw.AddRectFilled(
|
||||||
padMin,
|
padMin,
|
||||||
|
|||||||
@@ -317,6 +317,13 @@ internal sealed class Sidebar
|
|||||||
),
|
),
|
||||||
AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors),
|
AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors),
|
||||||
BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity),
|
BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity),
|
||||||
|
},
|
||||||
|
new RowStyle
|
||||||
|
{
|
||||||
|
// Across the gap between the sidebar group and the message
|
||||||
|
// area, so the selected tab touches the conversation it
|
||||||
|
// selects instead of ending at the seam.
|
||||||
|
ActiveBridgeWidth = ImGui.GetStyle().ItemSpacing.X,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,30 @@ internal sealed class StatusBar
|
|||||||
FontAwesomeIcon.Lock,
|
FontAwesomeIcon.Lock,
|
||||||
_fonts.FontAwesome
|
_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))
|
if (Fits(privacyLabel, withDot: false, lockWidth))
|
||||||
x +=
|
x +=
|
||||||
DrawSlot(
|
DrawSlot(
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ internal static class HoverState
|
|||||||
// OR, never assign: a second caller in the same frame that is not hovered
|
// OR, never assign: a second caller in the same frame that is not hovered
|
||||||
// must not cancel the first one that is.
|
// must not cancel the first one that is.
|
||||||
entry.Hovered |= hovered;
|
entry.Hovered |= hovered;
|
||||||
return entry.Value;
|
|
||||||
|
// Callers get the eased value; the entry keeps the linear one, which is
|
||||||
|
// what the evict rule and the advance step are written against.
|
||||||
|
return HoverMath.Ease(entry.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Never re-entrant: BeginFrame runs once from the draw thread, AdvanceForTest
|
// Never re-entrant: BeginFrame runs once from the draw thread, AdvanceForTest
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ internal static class Metrics
|
|||||||
// --- Input bar ---
|
// --- Input bar ---
|
||||||
internal const float InputBarHeightRaw = 32f;
|
internal const float InputBarHeightRaw = 32f;
|
||||||
|
|
||||||
// Raised from 130 in v1.13.0 for the screenshot-mode button. Five buttons in
|
// Two ghost buttons since v1.14.0 -- symbols and the more-menu. Everything
|
||||||
// the main window (symbols, theme, screenshot, settings, hide) and five in a
|
// else moved into the menu, and the hundred-odd pixels went back to the
|
||||||
// pop-out, where pop-in takes the place of hide.
|
// input field.
|
||||||
internal const float InputQuickButtonsReserveRaw = 156f;
|
internal const float InputQuickButtonsReserveRaw = 62f;
|
||||||
|
|
||||||
// --- Honorific header ---
|
// --- Honorific header ---
|
||||||
internal const float HonorificHeightRaw = 30f;
|
internal const float HonorificHeightRaw = 30f;
|
||||||
|
|||||||
@@ -9,11 +9,17 @@ namespace HellionChat.Ui.StyleEngine.Widgets;
|
|||||||
// The band above the conversation: which channel you are in on the left, where
|
// The band above the conversation: which channel you are in on the left, where
|
||||||
// you are and what time it is on the right.
|
// you are and what time it is on the right.
|
||||||
//
|
//
|
||||||
// Set apart by small caps with wide tracking rather than by size. That is a
|
// Set apart by small caps with wide tracking, and by nothing else. The mockup
|
||||||
// deliberate departure from the mockup, which asks for one pixel smaller than
|
// draws this as a filled bar; the settings window used to draw its section
|
||||||
// body text: one pixel would cost a whole additional font handle at full glyph
|
// headings that way too, and it was dropped in v1.11.0 for a reason worth
|
||||||
// range, because tab names are free user input and can be CJK. Tracking carries
|
// repeating -- a fill reads fine against blue themes and vanishes against violet
|
||||||
// the same weight in every palette and costs nothing.
|
// ones, because its only distinction from its surroundings is hue. Tracking is
|
||||||
|
// shape, and shape survives every palette.
|
||||||
|
//
|
||||||
|
// So: no plate, but a tint. A tenth-opacity accent wash falling from the top
|
||||||
|
// edge -- colour as atmosphere rather than as a box, which is what the plate got
|
||||||
|
// wrong. Flo picked it from the lab over the rule-only variant; at this strength
|
||||||
|
// it survives the violet themes that killed the filled bar.
|
||||||
//
|
//
|
||||||
// Which face draws what is not a style choice here, it is a constraint. The meta
|
// Which face draws what is not a style choice here, it is a constraint. The meta
|
||||||
// face has a glyph range of ASCII plus a middle dot, so only the world name and
|
// face has a glyph range of ASCII plus a middle dot, so only the world name and
|
||||||
@@ -68,8 +74,10 @@ internal static class ChannelHeader
|
|||||||
var origin = ImGui.GetCursorScreenPos();
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
|
||||||
var theme = Plugin.Instance.ThemeRegistry.Active;
|
var theme = Plugin.Instance.ThemeRegistry.Active;
|
||||||
var surface = theme.Colors.Surface;
|
// Contrast is still measured against the surface the header sits on, even
|
||||||
var surfaceAbgr = ColourUtil.RgbaToAbgr(surface);
|
// though it no longer paints one -- the chat log's own floor is what the
|
||||||
|
// text lands on.
|
||||||
|
var surfaceAbgr = ColourUtil.RgbaToAbgr(theme.Colors.ChildBg);
|
||||||
|
|
||||||
var body = BodyFace(fonts);
|
var body = BodyFace(fonts);
|
||||||
var meta = MetaFace(fonts);
|
var meta = MetaFace(fonts);
|
||||||
@@ -152,27 +160,23 @@ internal static class ChannelHeader
|
|||||||
|
|
||||||
var dl = ImGui.GetWindowDrawList();
|
var dl = ImGui.GetWindowDrawList();
|
||||||
var bottomRight = origin + new Vector2(width, height);
|
var bottomRight = origin + new Vector2(width, height);
|
||||||
dl.AddRectFilled(origin, bottomRight, surfaceAbgr);
|
|
||||||
dl.AddLine(
|
// Converted first, then measured. EnsureContrast works in ABGR --
|
||||||
new Vector2(origin.X, bottomRight.Y - 1f),
|
// handing it the theme's RGBA swaps red and blue on both arguments,
|
||||||
new Vector2(bottomRight.X, bottomRight.Y - 1f),
|
// so it measures a contrast that has nothing to do with what ends up
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Border),
|
// on screen and returns a colour in the wrong order on top.
|
||||||
MathF.Max(1f, scale)
|
var accent = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(theme.Colors.Accent),
|
||||||
|
surfaceAbgr,
|
||||||
|
4.5f
|
||||||
);
|
);
|
||||||
|
|
||||||
|
dl.DrawEdgeTint(origin, bottomRight, ColourUtil.ApplyAlpha(accent, 0.10f), height);
|
||||||
|
|
||||||
var textY = origin.Y + MathF.Round(PadYRaw * scale);
|
var textY = origin.Y + MathF.Round(PadYRaw * scale);
|
||||||
|
|
||||||
if (plan.ShowName)
|
if (plan.ShowName)
|
||||||
{
|
{
|
||||||
// Converted first, then measured. EnsureContrast works in ABGR --
|
|
||||||
// handing it the theme's RGBA swaps red and blue on both arguments,
|
|
||||||
// so it measures a contrast that has nothing to do with what ends up
|
|
||||||
// on screen and returns a colour in the wrong order on top.
|
|
||||||
var accent = ColourUtil.EnsureContrast(
|
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Accent),
|
|
||||||
surfaceAbgr,
|
|
||||||
4.5f
|
|
||||||
);
|
|
||||||
var x = origin.X + inset;
|
var x = origin.X + inset;
|
||||||
|
|
||||||
// Centred against the band, not aligned to the text baseline:
|
// Centred against the band, not aligned to the text baseline:
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
// The popup must be sized from its rows, not the rows from the popup: an
|
||||||
|
// ImGui popup does not grow for draw-list content, so a fixed minimum width
|
||||||
|
// clips whichever locale writes the longest labels. German did, first day.
|
||||||
|
internal static float CalcWidth(string label, FontAwesomeIcon? icon, FontManager fonts)
|
||||||
|
{
|
||||||
|
var scale = Metrics.Scale;
|
||||||
|
var w = PadXRaw * 2f * scale + ImGui.CalcTextSize(label).X;
|
||||||
|
|
||||||
|
if (icon is { } glyph)
|
||||||
|
{
|
||||||
|
using (fonts.FontAwesome.Push())
|
||||||
|
w += ImGui.CalcTextSize(glyph.ToIconString()).X + IconGapRaw * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.TextPrimary),
|
||||||
|
ColourUtil.RgbaToAbgr(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,12 @@ internal readonly record struct RowStyle
|
|||||||
|
|
||||||
public float AccentBarWidth { get; init; } = 2f;
|
public float AccentBarWidth { get; init; } = 2f;
|
||||||
public bool DrawSeparator { get; init; } = true;
|
public bool DrawSeparator { get; init; } = true;
|
||||||
|
|
||||||
|
// Extends the active fill past the right edge by this many pixels, so the
|
||||||
|
// selected tab reads as attached to the content it selects -- the classic
|
||||||
|
// tab metaphor -- instead of ending at a hard seam. Zero for rows that do
|
||||||
|
// not sit next to the thing they select.
|
||||||
|
public float ActiveBridgeWidth { get; init; } = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class Row
|
internal static class Row
|
||||||
@@ -51,13 +57,19 @@ internal static class Row
|
|||||||
// default is 0.85 -- an opaque fill per row would make the sidebar a
|
// default is 0.85 -- an opaque fill per row would make the sidebar a
|
||||||
// solid block inside a translucent window.
|
// solid block inside a translucent window.
|
||||||
if (state.IsActive)
|
if (state.IsActive)
|
||||||
dl.AddRectFilled(origin, max, state.SurfaceActiveAbgr);
|
{
|
||||||
|
var fillMax = max;
|
||||||
|
if (style.ActiveBridgeWidth > 0f)
|
||||||
|
fillMax.X += style.ActiveBridgeWidth;
|
||||||
|
dl.AddRectFilled(origin, fillMax, state.SurfaceActiveAbgr);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.HoverAmount > 0f)
|
if (state.HoverAmount > 0f)
|
||||||
dl.AddRectFilled(
|
dl.AddRectFilled(
|
||||||
origin,
|
origin,
|
||||||
max,
|
max,
|
||||||
ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount)
|
ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount),
|
||||||
|
3f * Metrics.Scale
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.IsActive && style.AccentBarWidth > 0f)
|
if (state.IsActive && style.AccentBarWidth > 0f)
|
||||||
@@ -72,12 +84,17 @@ internal static class Row
|
|||||||
// path, so an unscaled 1px offset with a scaled stroke puts half the
|
// path, so an unscaled 1px offset with a scaled stroke puts half the
|
||||||
// line below max.Y -- and rows stack flush, so that half lands in the
|
// line below max.Y -- and rows stack flush, so that half lands in the
|
||||||
// first pixel row of the next one.
|
// first pixel row of the next one.
|
||||||
|
// A full-width line under every row reads as a ladder of hard
|
||||||
|
// cuts -- the tester's words were that the list felt too hard.
|
||||||
|
// The rule now starts past the icon column and fades to nothing
|
||||||
|
// before the right edge, the same shape the section headers use.
|
||||||
var thickness = Metrics.Scale;
|
var thickness = Metrics.Scale;
|
||||||
var y = max.Y - thickness * 0.5f;
|
var y = max.Y - thickness * 0.5f;
|
||||||
dl.AddLine(
|
var inset = 10f * Metrics.Scale;
|
||||||
new Vector2(origin.X, y),
|
dl.DrawFadeRule(
|
||||||
new Vector2(max.X, y),
|
new Vector2(origin.X + inset, y),
|
||||||
state.BorderAbgr,
|
size.X - inset,
|
||||||
|
ColourUtil.ApplyAlpha(state.BorderAbgr, 0.55f),
|
||||||
thickness
|
thickness
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Ui.StyleEngine;
|
||||||
|
using HellionChat.Ui.StyleEngine.Widgets;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.Windows;
|
||||||
|
|
||||||
|
// Variants of UI elements, drawn side by side so decisions are made by looking
|
||||||
|
// rather than by imagining. Reachable with /hellion lab.
|
||||||
|
//
|
||||||
|
// Permanent, by Flo's call: a dev playground for seeing ideas in-game against
|
||||||
|
// the live theme. The radios default to whatever shipped, so the window also
|
||||||
|
// documents which variant won and what it beat.
|
||||||
|
//
|
||||||
|
// It exists because the alternative was drawing mockups, and mockups are what
|
||||||
|
// sent this cycle down the wrong path once already: they are from before the
|
||||||
|
// settings window learned that structure is typography and only controls get a
|
||||||
|
// fill. Everything here is real ImGui against the live theme, so switching
|
||||||
|
// themes switches the answer too -- which is the part a picture cannot show.
|
||||||
|
//
|
||||||
|
// The tools were all sitting in DrawListExtensions unused: DrawSlipPolygon and
|
||||||
|
// DrawVerticalGradient had one caller between them (SegmentedControl),
|
||||||
|
// DrawGlowBorder had none at all.
|
||||||
|
internal sealed class InputBarLabWindow : Window
|
||||||
|
{
|
||||||
|
private readonly Plugin _plugin;
|
||||||
|
|
||||||
|
private string _sample = string.Empty;
|
||||||
|
private int _buttonVariant = 2;
|
||||||
|
private int _fieldVariant = 2;
|
||||||
|
private int _pillVariant = 2;
|
||||||
|
private int _headerVariant = 2;
|
||||||
|
|
||||||
|
internal InputBarLabWindow(Plugin plugin)
|
||||||
|
: base("Input Bar Lab###hellion-input-lab")
|
||||||
|
{
|
||||||
|
_plugin = plugin;
|
||||||
|
Size = new Vector2(560, 720);
|
||||||
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
var fonts = _plugin.FontManager;
|
||||||
|
if (fonts is null || !fonts.FontsReady)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("fonts not ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = _plugin.ThemeRegistry.Active.Colors;
|
||||||
|
|
||||||
|
ImGui.TextDisabled(
|
||||||
|
$"theme {_plugin.ThemeRegistry.Active.Slug} · scale {Metrics.Scale:0.00}"
|
||||||
|
);
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
DrawButtonSection(c, fonts);
|
||||||
|
DrawFieldSection(c);
|
||||||
|
DrawPillSection(c, fonts);
|
||||||
|
DrawHeaderSection(c, fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- icon buttons -------------------------------------------------------
|
||||||
|
|
||||||
|
private void DrawButtonSection(ThemeColors c, FontManager fonts)
|
||||||
|
{
|
||||||
|
Heading("ICON BUTTONS", c);
|
||||||
|
ImGui.RadioButton("plain hover##b", ref _buttonVariant, 0);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("chamfer + gradient##b", ref _buttonVariant, 1);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("glow on hover##b", ref _buttonVariant, 2);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var icons = new[]
|
||||||
|
{
|
||||||
|
FontAwesomeIcon.SmileBeam,
|
||||||
|
FontAwesomeIcon.Palette,
|
||||||
|
FontAwesomeIcon.Cog,
|
||||||
|
FontAwesomeIcon.Camera,
|
||||||
|
FontAwesomeIcon.EyeSlash,
|
||||||
|
};
|
||||||
|
|
||||||
|
var scale = Metrics.Scale;
|
||||||
|
var side = MathF.Round(26f * scale);
|
||||||
|
var gap = MathF.Round(4f * scale);
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
for (var i = 0; i < icons.Length; i++)
|
||||||
|
{
|
||||||
|
var min = new Vector2(origin.X + i * (side + gap), origin.Y);
|
||||||
|
var max = min + new Vector2(side, side);
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(min);
|
||||||
|
ImGui.InvisibleButton($"##lab-btn-{i}", new Vector2(side, side));
|
||||||
|
var hovered = ImGui.IsItemHovered();
|
||||||
|
var amount = HoverState.Query((uint)(0x1B000 + i), hovered);
|
||||||
|
|
||||||
|
DrawIconButtonVariant(dl, min, max, icons[i], amount, c, fonts, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(origin);
|
||||||
|
ImGui.Dummy(new Vector2(0f, side + 10f * scale));
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawIconButtonVariant(
|
||||||
|
ImDrawListPtr dl,
|
||||||
|
Vector2 min,
|
||||||
|
Vector2 max,
|
||||||
|
FontAwesomeIcon icon,
|
||||||
|
float amount,
|
||||||
|
ThemeColors c,
|
||||||
|
FontManager fonts,
|
||||||
|
float scale
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var accent = ColourUtil.RgbaToAbgr(c.Accent);
|
||||||
|
|
||||||
|
switch (_buttonVariant)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
// What the sidebar does: nothing at rest, a soft fill on hover.
|
||||||
|
if (amount > 0f)
|
||||||
|
dl.AddRectFilled(
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount),
|
||||||
|
3f * scale
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// Chamfered plate with a vertical gradient, the shape the
|
||||||
|
// segmented control uses for its selected segment.
|
||||||
|
dl.DrawSlipPolygon(min, max, c.Surface, 4f * scale);
|
||||||
|
dl.DrawVerticalGradient(
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
ColourUtil.ApplyAlpha(
|
||||||
|
ColourUtil.RgbaToAbgr(c.SurfaceHover),
|
||||||
|
0.35f + amount * 0.4f
|
||||||
|
),
|
||||||
|
0u
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// Flat at rest, and the glow border -- unused until now -- comes
|
||||||
|
// up with the hover instead of a fill.
|
||||||
|
if (amount > 0f)
|
||||||
|
{
|
||||||
|
dl.AddRectFilled(
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.Surface), amount * 0.8f),
|
||||||
|
3f * scale
|
||||||
|
);
|
||||||
|
// Alpha into the LOW byte: DrawGlowBorder reads RGBA, while
|
||||||
|
// ApplyAlpha writes the high byte (ABGR). The first version
|
||||||
|
// of this lab mixed them up and got a full-alpha glow with
|
||||||
|
// the red channel dimmed instead.
|
||||||
|
var glow = (c.Accent & 0xFFFFFF00u) | (uint)(byte)Math.Round(0xB4 * amount);
|
||||||
|
dl.DrawGlowBorder(min, max, glow, 1f, 4);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (fonts.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
var glyph = icon.ToIconString();
|
||||||
|
var size = ImGui.CalcTextSize(glyph);
|
||||||
|
var tint = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.TextPrimary),
|
||||||
|
ColourUtil.RgbaToAbgr(c.ChildBg),
|
||||||
|
4.5f
|
||||||
|
);
|
||||||
|
if (amount > 0f)
|
||||||
|
tint = ColourUtil.Lerp(tint, accent, amount);
|
||||||
|
|
||||||
|
dl.AddText(min + (max - min - size) * 0.5f, tint, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- input field --------------------------------------------------------
|
||||||
|
|
||||||
|
private void DrawFieldSection(ThemeColors c)
|
||||||
|
{
|
||||||
|
Heading("INPUT FIELD", c);
|
||||||
|
ImGui.RadioButton("imgui default##f", ref _fieldVariant, 0);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("own surface##f", ref _fieldVariant, 1);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("own surface + focus rail##f", ref _fieldVariant, 2);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var scale = Metrics.Scale;
|
||||||
|
var height = MathF.Round(24f * scale);
|
||||||
|
var width = ImGui.GetContentRegionAvail().X - 8f;
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
var max = origin + new Vector2(width, height);
|
||||||
|
|
||||||
|
if (_fieldVariant > 0)
|
||||||
|
{
|
||||||
|
// The technique from Boutique.Inputs: paint the surface, then hand
|
||||||
|
// ImGui a transparent frame so the widget draws only text and caret.
|
||||||
|
dl.AddRectFilled(origin, max, ColourUtil.RgbaToAbgr(c.FrameBg), 6f * scale);
|
||||||
|
|
||||||
|
if (_fieldVariant == 2 && ImGui.IsItemActive())
|
||||||
|
dl.AddRectFilled(
|
||||||
|
origin,
|
||||||
|
new Vector2(origin.X + 2f * scale, max.Y),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero, _fieldVariant > 0))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.FrameBgHovered, Vector4.Zero, _fieldVariant > 0))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.FrameBgActive, Vector4.Zero, _fieldVariant > 0))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(10f * scale, 4f * scale)))
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(width);
|
||||||
|
ImGui.InputTextWithHint("##lab-field", "Type a message...", ref _sample, 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus rail after the widget: IsItemActive only answers afterwards.
|
||||||
|
if (_fieldVariant == 2 && ImGui.IsItemActive())
|
||||||
|
dl.AddRectFilled(
|
||||||
|
origin,
|
||||||
|
new Vector2(origin.X + 2f * scale, max.Y),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent)
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- channel pill -------------------------------------------------------
|
||||||
|
|
||||||
|
private void DrawPillSection(ThemeColors c, FontManager fonts)
|
||||||
|
{
|
||||||
|
Heading("CHANNEL PILL", c);
|
||||||
|
ImGui.RadioButton("current##p", ref _pillVariant, 0);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("gradient + top light##p", ref _pillVariant, 1);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("chamfered##p", ref _pillVariant, 2);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var scale = Metrics.Scale;
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
const string label = "Jingliu Moon";
|
||||||
|
var textWidth = ImGui.CalcTextSize(label).X;
|
||||||
|
var size = new Vector2(textWidth + 34f * scale, MathF.Round(22f * scale));
|
||||||
|
var max = origin + size;
|
||||||
|
var fill = ColourUtil.RgbaToAbgr(c.Accent);
|
||||||
|
|
||||||
|
switch (_pillVariant)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
dl.AddRectFilled(origin, max, fill, 6f * scale);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
dl.AddRectFilled(origin, max, fill, 6f * scale);
|
||||||
|
dl.DrawVerticalGradient(origin, max, 0x30FFFFFFu, 0u);
|
||||||
|
dl.AddRectFilled(
|
||||||
|
new Vector2(origin.X + 6f * scale, origin.Y),
|
||||||
|
new Vector2(max.X - 6f * scale, origin.Y + 1f * scale),
|
||||||
|
0x60FFFFFFu
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
dl.DrawSlipPolygon(origin, max, c.Accent, 6f * scale);
|
||||||
|
dl.DrawVerticalGradient(origin, max, 0x28FFFFFFu, 0u);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ink = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.WindowBg),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent),
|
||||||
|
4.5f
|
||||||
|
);
|
||||||
|
using (fonts.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
var arrow = FontAwesomeIcon.ArrowRight.ToIconString();
|
||||||
|
dl.AddText(
|
||||||
|
new Vector2(
|
||||||
|
origin.X + 8f * scale,
|
||||||
|
origin.Y + (size.Y - ImGui.GetFontSize()) * 0.5f
|
||||||
|
),
|
||||||
|
ink,
|
||||||
|
arrow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.AddText(
|
||||||
|
new Vector2(
|
||||||
|
origin.X + 24f * scale,
|
||||||
|
origin.Y + (size.Y - ImGui.GetTextLineHeight()) * 0.5f
|
||||||
|
),
|
||||||
|
ink,
|
||||||
|
label
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(0f, size.Y + 10f * scale));
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- channel header -----------------------------------------------------
|
||||||
|
|
||||||
|
private void DrawHeaderSection(ThemeColors c, FontManager fonts)
|
||||||
|
{
|
||||||
|
Heading("CHANNEL HEADER", c);
|
||||||
|
ImGui.RadioButton("rule only (current)##h", ref _headerVariant, 0);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("underline##h", ref _headerVariant, 1);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.RadioButton("tinted band##h", ref _headerVariant, 2);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var scale = Metrics.Scale;
|
||||||
|
var width = ImGui.GetContentRegionAvail().X - 8f;
|
||||||
|
var height = ImGui.GetTextLineHeight() + 14f * scale;
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var max = origin + new Vector2(width, height);
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
var accent = ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent),
|
||||||
|
ColourUtil.RgbaToAbgr(c.ChildBg),
|
||||||
|
4.5f
|
||||||
|
);
|
||||||
|
|
||||||
|
// The band variant is the one the mockup drew and v1.11.0 abandoned.
|
||||||
|
// Here at a fraction of the strength, as a tint rather than a plate --
|
||||||
|
// included so the comparison is fair rather than rhetorical.
|
||||||
|
if (_headerVariant == 2)
|
||||||
|
dl.DrawEdgeTint(origin, max, ColourUtil.ApplyAlpha(accent, 0.10f), height);
|
||||||
|
|
||||||
|
var textY = origin.Y + 7f * scale;
|
||||||
|
const string name = "FREE COMPANY";
|
||||||
|
|
||||||
|
float drawn;
|
||||||
|
using (fonts.MetaFont!.Push())
|
||||||
|
drawn = dl.DrawTrackedText(
|
||||||
|
new Vector2(origin.X + 14f * scale, textY),
|
||||||
|
name,
|
||||||
|
accent,
|
||||||
|
1.8f * scale
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_headerVariant == 0)
|
||||||
|
{
|
||||||
|
var ruleX = origin.X + 14f * scale + drawn + 10f * scale;
|
||||||
|
if (max.X - 14f * scale > ruleX)
|
||||||
|
dl.DrawFadeRule(
|
||||||
|
new Vector2(ruleX, textY + ImGui.GetTextLineHeight() * 0.5f),
|
||||||
|
max.X - 14f * scale - ruleX,
|
||||||
|
ColourUtil.RgbaToAbgr(c.Border),
|
||||||
|
MathF.Max(1f, scale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (_headerVariant == 1)
|
||||||
|
{
|
||||||
|
// Underline under the name only, the way the top tab strip marks its
|
||||||
|
// active tab -- a shape the plugin already uses elsewhere.
|
||||||
|
dl.AddRectFilled(
|
||||||
|
new Vector2(origin.X + 14f * scale, max.Y - 2f * scale),
|
||||||
|
new Vector2(origin.X + 14f * scale + drawn, max.Y),
|
||||||
|
accent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(0f, height + 6f * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- shared -------------------------------------------------------------
|
||||||
|
|
||||||
|
private static void Heading(string text, ThemeColors c)
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
var pos = ImGui.GetCursorScreenPos();
|
||||||
|
var width = dl.DrawTrackedText(
|
||||||
|
pos,
|
||||||
|
text,
|
||||||
|
ColourUtil.EnsureContrast(
|
||||||
|
ColourUtil.RgbaToAbgr(c.TextMuted),
|
||||||
|
ColourUtil.RgbaToAbgr(c.WindowBg),
|
||||||
|
4.5f
|
||||||
|
),
|
||||||
|
1.6f * Metrics.Scale
|
||||||
|
);
|
||||||
|
|
||||||
|
var ruleX = pos.X + width + 10f;
|
||||||
|
var avail = ImGui.GetContentRegionAvail().X;
|
||||||
|
dl.DrawFadeRule(
|
||||||
|
new Vector2(ruleX, pos.Y + ImGui.GetTextLineHeight() * 0.5f),
|
||||||
|
pos.X + avail - ruleX,
|
||||||
|
ColourUtil.RgbaToAbgr(c.Border),
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(0f, ImGui.GetTextLineHeight() + 4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -367,6 +367,25 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
_sidebar.Draw(bodyWidth, tabs, ref _activeTab);
|
_sidebar.Draw(bodyWidth, tabs, ref _activeTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The gap between the tab list and the conversation used to be a bare
|
||||||
|
// strip of window background with a hard edge on both sides -- tester
|
||||||
|
// feedback called the transition too hard. A faint surface wash that
|
||||||
|
// fades toward the messages turns the cut into a seam. The active row
|
||||||
|
// bridges across it (RowStyle.ActiveBridgeWidth), so the selected tab
|
||||||
|
// stays attached to its content on top of the wash.
|
||||||
|
{
|
||||||
|
var seamMin = new Vector2(ImGui.GetItemRectMax().X, ImGui.GetItemRectMin().Y);
|
||||||
|
var seamMax = new Vector2(
|
||||||
|
seamMin.X + ImGui.GetStyle().ItemSpacing.X,
|
||||||
|
ImGui.GetItemRectMax().Y
|
||||||
|
);
|
||||||
|
var wash = ColourUtil.ApplyAlpha(
|
||||||
|
ColourUtil.RgbaToAbgr(Plugin.Instance.ThemeRegistry.Active.Colors.Surface),
|
||||||
|
0.35f
|
||||||
|
);
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilledMultiColor(seamMin, seamMax, wash, 0u, 0u, wash);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
using (ImRaii.Group())
|
using (ImRaii.Group())
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
namespace HellionChat.Util;
|
namespace HellionChat.Util;
|
||||||
|
|
||||||
// State rules for the held hover value, split from HoverState so the build suite
|
// State rules for the held hover value, split from HoverState so the build suite
|
||||||
// can pin them without an ImGui frame. Rates follow Lightless (Selune.cs:36-37):
|
// can pin them without an ImGui frame. Slower out than in is what makes a fade
|
||||||
// slower out than in is what makes a fade read as deliberate rather than laggy.
|
// read as deliberate rather than laggy (the asymmetry follows Lightless,
|
||||||
|
// Selune.cs:36-37).
|
||||||
|
//
|
||||||
|
// The rates started at 14/s in and 8/s out -- a full fade-in in seventy
|
||||||
|
// milliseconds, which tester feedback called light speed. The first correction
|
||||||
|
// halved them and was still "much too fast", so: rise in about a third of a
|
||||||
|
// second, settle in half of one. Textbook says 150ms; the textbook does not
|
||||||
|
// play this game.
|
||||||
internal static class HoverMath
|
internal static class HoverMath
|
||||||
{
|
{
|
||||||
internal const float FadeInPerSecond = 14f;
|
internal const float FadeInPerSecond = 3f;
|
||||||
internal const float FadeOutPerSecond = 8f;
|
internal const float FadeOutPerSecond = 2f;
|
||||||
|
|
||||||
// Below this an entry is indistinguishable from zero and can be dropped.
|
// Below this an entry is indistinguishable from zero and can be dropped.
|
||||||
internal const float EvictBelow = 0.001f;
|
internal const float EvictBelow = 0.001f;
|
||||||
@@ -20,4 +27,9 @@ internal static class HoverMath
|
|||||||
);
|
);
|
||||||
|
|
||||||
internal static bool ShouldEvict(float value, bool hovered) => !hovered && value <= EvictBelow;
|
internal static bool ShouldEvict(float value, bool hovered) => !hovered && value <= EvictBelow;
|
||||||
|
|
||||||
|
// Smoothstep over the linear state. The state itself stays linear -- evict
|
||||||
|
// and threshold logic depend on it -- but what callers paint with starts
|
||||||
|
// gently and lands gently instead of moving at one speed and stopping dead.
|
||||||
|
internal static float Ease(float t) => t * t * (3f - 2f * t);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,83 @@ releases as an overview and links to the release pages for details.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.14.0] — unreleased (local only)
|
||||||
|
|
||||||
|
The input row. It was the one surface a user actually works in and it spoke three
|
||||||
|
shape languages at once — a rounded pill, a square field, five filled default
|
||||||
|
buttons — sitting directly above a status bar that is rounded throughout. This
|
||||||
|
release makes the row one thing, and it teaches the popups the window's own
|
||||||
|
shapes while it is in there. **Not published** — the public release stays at
|
||||||
|
v1.5.6.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A style lab under `/hellion lab`: variants of UI elements side by side, drawn
|
||||||
|
in real ImGui against the live theme. Permanent, as a dev playground — its
|
||||||
|
radio buttons default to whatever shipped, so the window doubles as a record
|
||||||
|
of which variant won. Every decision in this release was made by looking at it
|
||||||
|
in-game rather than at a drawing.
|
||||||
|
- `PopupRow`, the row a popup is made of: soft hover fill, a two-pixel accent
|
||||||
|
bar on the active entry — the sidebar row's shape, which is also exactly what
|
||||||
|
Character Select+ draws for its own popup entries. The channel picker and the
|
||||||
|
new input-row menu are built from it.
|
||||||
|
- A colour-channel guard in preflight (Block G). `EnsureContrast` works in ABGR,
|
||||||
|
theme colours are RGBA, both are `uint`, and the mistake of feeding one into
|
||||||
|
the other happened six times in a single day — each found only by a human
|
||||||
|
staring at unreadable text. The guard flags any raw theme member reaching the
|
||||||
|
helper; on its first real run it caught three cases a hand-written search had
|
||||||
|
missed minutes earlier.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The five filled buttons in the input row are two ghost buttons and a menu.
|
||||||
|
Symbols stay outside because they are used mid-sentence; theme, settings,
|
||||||
|
screenshot mode and hide moved in. The ghosts are flat at rest and glow on
|
||||||
|
hover — the first caller `DrawGlowBorder` ever had.
|
||||||
|
- The screenshot toggle was allowed into the menu only because its state moved
|
||||||
|
to the status bar first, as an accent camera pill that shows while the mode is
|
||||||
|
on. Knowing what the screen shows before the screenshot key is pressed is the
|
||||||
|
entire point of the mode; a 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. Focus is a two-pixel accent
|
||||||
|
rail on the left edge — the same mark rows and popups use. The field is about
|
||||||
|
a hundred pixels wider, reclaimed from the button reserve.
|
||||||
|
- The channel pill is chamfered, the corner language of the segmented control,
|
||||||
|
with a faint white depth gradient.
|
||||||
|
- The channel header traded its surface plate for a tenth-opacity accent wash.
|
||||||
|
The plate was the exact shape the settings window abandoned in v1.11.0 — it
|
||||||
|
reads fine on blue themes and vanishes on violet ones.
|
||||||
|
- The sidebar softened, on tester feedback that the transition to the chat field
|
||||||
|
was too hard: the full-width border line under every row is a fading rule now,
|
||||||
|
the gap between list and messages carries a surface wash instead of two hard
|
||||||
|
edges, and the selected tab bridges across it so it touches the conversation
|
||||||
|
it selects.
|
||||||
|
- Hover fades run at a quarter of their old speed with a smoothstep curve —
|
||||||
|
soft start, soft landing. 14/s in was judged light speed; it is 3/s now, and
|
||||||
|
a test guards against drifting back. Every hover in the plugin inherits both
|
||||||
|
changes through the one shared code path.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Popups size themselves from their widest visible entry. Both new popups
|
||||||
|
carried a guessed minimum width, and German outran it within a day —
|
||||||
|
`Schnellauswahl` and `Chat ausblenden` were clipped mid-word.
|
||||||
|
- Popup entry text was unreadable at rest on several themes and only became
|
||||||
|
legible on hover: the RGBA-into-ABGR mistake again, in `PopupRow` and three
|
||||||
|
spots in the lab. Fixed, and the new preflight guard exists so the seventh
|
||||||
|
occurrence dies in CI rather than in a screenshot.
|
||||||
|
- The pill label was raw theme text on an accent fill with no contrast check.
|
||||||
|
|
||||||
|
### Known issues
|
||||||
|
|
||||||
|
- The channel header still recomputes its widths every frame rather than on the
|
||||||
|
status bar's one-second tick.
|
||||||
|
- `Metrics.Scale` still calls a Dalamud property that is marked obsolete.
|
||||||
|
- The first-run wizard is now the last surface drawn in ImGui defaults. It is
|
||||||
|
the next cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.13.0] — unreleased (local only)
|
## [1.13.0] — unreleased (local only)
|
||||||
|
|
||||||
Typography. The message list was the last surface still drawn in ImGui defaults,
|
Typography. The message list was the last surface still drawn in ImGui defaults,
|
||||||
|
|||||||
+13
-3
@@ -14,7 +14,7 @@ be a poor fit for the plugin's privacy-first scope during brainstorming.
|
|||||||
|
|
||||||
The published release is **v1.5.6**. Development since then runs as a UI rebuild towards v2.0.0 and
|
The published release is **v1.5.6**. Development since then runs as a UI rebuild towards v2.0.0 and
|
||||||
is not published: the whole window layer is being rewritten from ImGui defaults to custom drawing.
|
is not published: the whole window layer is being rewritten from ImGui defaults to custom drawing.
|
||||||
Versions v1.6.0 through v1.13.0 are local development states, and `repo.json` deliberately keeps
|
Versions v1.6.0 through v1.14.0 are local development states, and `repo.json` deliberately keeps
|
||||||
its download links on v1.5.6 so nobody updates into a partial state.
|
its download links on v1.5.6 so nobody updates into a partial state.
|
||||||
|
|
||||||
Where it stands:
|
Where it stands:
|
||||||
@@ -35,8 +35,18 @@ Where it stands:
|
|||||||
- **v1.13.0** — typography. Named type roles instead of one size, a channel header above the
|
- **v1.13.0** — typography. Named type roles instead of one size, a channel header above the
|
||||||
conversation, timestamps in a column of their own, and rows with a surface. Screenshot mode
|
conversation, timestamps in a column of their own, and rows with a surface. Screenshot mode
|
||||||
reached one of four surfaces that draw a tab name and now reaches all of them.
|
reached one of four surfaces that draw a tab name and now reaches all of them.
|
||||||
- **v1.14.0 onwards** — the sidebar moving from tab rows to channel rows, then the first-run
|
- **v1.14.0** — the input row: two ghost buttons and a menu where five filled defaults sat, a
|
||||||
wizard and the input bar, which are the last two surfaces still drawn in ImGui defaults.
|
field with its own surface and focus rail, popups built from the sidebar's row shape, and a
|
||||||
|
softened seam between the tab list and the conversation. Plus a style lab under
|
||||||
|
`/hellion lab` and a preflight guard for the RGBA/ABGR contrast trap.
|
||||||
|
- **v1.15.0** — the first-run wizard, the last surface still drawn in ImGui defaults, and the
|
||||||
|
first thing a tester sees.
|
||||||
|
|
||||||
|
The sidebar is no longer on this list. Earlier drafts had it moving from tab rows to channel
|
||||||
|
rows, but both reasons for that are gone: it was flat and unstyled when the idea was written,
|
||||||
|
and it is neither now — it draws its own row surfaces and already groups pinned and auto-tell
|
||||||
|
tabs under headings. What remained was a change to how the plugin is operated rather than how
|
||||||
|
it looks, and tabs are what users have learned. They stay.
|
||||||
|
|
||||||
A tester beta follows once the visual pass is complete.
|
A tester beta follows once the visual pass is complete.
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ echo "==> preflight: Block B — manifest shape"
|
|||||||
echo "==> preflight: Block C — changelog sync"
|
echo "==> preflight: Block C — changelog sync"
|
||||||
./scripts/verify-changelog-sync.sh
|
./scripts/verify-changelog-sync.sh
|
||||||
|
|
||||||
|
echo "==> preflight: Block G — colour channel audit"
|
||||||
|
./scripts/verify-colour-channels.sh
|
||||||
|
|
||||||
echo "==> preflight: Block D — plugin compile health"
|
echo "==> preflight: Block D — plugin compile health"
|
||||||
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
||||||
|
|
||||||
|
|||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-colour-channels.sh — Block G of preflight.
|
||||||
|
#
|
||||||
|
# EnsureContrast works in ABGR. ThemeColors fields are RGBA. Feeding one into
|
||||||
|
# the other swaps red and blue on both arguments, measures a contrast between
|
||||||
|
# two colours that are never on screen, and returns a value in the wrong order
|
||||||
|
# on top — and because RgbaToAbgr is an involution, a stray extra conversion
|
||||||
|
# around the call makes it LOOK plausible while being wrong.
|
||||||
|
#
|
||||||
|
# This happened four times on 2026-08-19 alone (channel header, ghost buttons,
|
||||||
|
# pill text, popup rows), each found only by a human staring at an unreadable
|
||||||
|
# window. The compiler cannot catch it — both layouts are uint. This script
|
||||||
|
# catches the one shape every occurrence shared: a raw ThemeColors member as a
|
||||||
|
# direct argument to EnsureContrast.
|
||||||
|
#
|
||||||
|
# Legitimate calls convert first (RgbaToAbgr(...)) or pass values that are
|
||||||
|
# already ABGR (fields/locals named *Abgr, palette.Abgr(...)).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
fail() { echo "verify-colour-channels: FAIL — $1" >&2; exit 1; }
|
||||||
|
ok() { echo "verify-colour-channels: OK — $1"; }
|
||||||
|
|
||||||
|
HITS="$(python3 - "$ROOT/HellionChat" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
root = Path(sys.argv[1])
|
||||||
|
# A ThemeColors member (via any local alias) fed straight into EnsureContrast,
|
||||||
|
# in either argument position, spanning line breaks. Members already in ABGR
|
||||||
|
# end in "Abgr" and are exempt.
|
||||||
|
pattern = re.compile(
|
||||||
|
r"EnsureContrast\(\s*"
|
||||||
|
r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*,"
|
||||||
|
r"|EnsureContrast\([^;]{0,200}?,\s*"
|
||||||
|
r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*[,)]",
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
|
||||||
|
hits = []
|
||||||
|
for f in root.rglob("*.cs"):
|
||||||
|
if "obj" in f.parts or "bin" in f.parts:
|
||||||
|
continue
|
||||||
|
text = f.read_text(encoding="utf-8")
|
||||||
|
for m in pattern.finditer(text):
|
||||||
|
line = text.count("\n", 0, m.start()) + 1
|
||||||
|
hits.append(f"{f.relative_to(root.parent)}:{line}")
|
||||||
|
|
||||||
|
print("\n".join(hits))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -n "$HITS" ]; then
|
||||||
|
fail "raw ThemeColors member fed into EnsureContrast (expects ABGR — wrap in RgbaToAbgr):
|
||||||
|
$HITS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "no raw RGBA theme members reach EnsureContrast"
|
||||||
Reference in New Issue
Block a user