feat(style): a lab window for the input-row decision
Four elements, three variants each, drawn side by side in real ImGui against the live theme. Reachable with /hellion lab. The alternative was drawing mockups, and mockups are what sent this cycle wrong once already -- they predate the settings window learning that structure is typography and only controls get a fill. A picture also cannot show the part that matters most here: switching themes switches which variant works. Everything it draws comes from helpers that were already in DrawListExtensions and had almost no callers. DrawSlipPolygon and DrawVerticalGradient had one between them, in SegmentedControl. DrawGlowBorder had none at all. Temporary, and deliberately not behind DEBUG: the decision happens in the build Flo runs. It comes out once the variants are picked.
This commit is contained in:
@@ -112,6 +112,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
#if DEBUG
|
||||
internal Ui.Windows.WidgetGalleryWindow WidgetGallery { get; private set; } = null!;
|
||||
#endif
|
||||
internal Ui.Windows.InputBarLabWindow InputBarLab { get; private set; } = null!;
|
||||
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
|
||||
internal DebuggerWindow DebuggerWindow { get; private set; } = null!;
|
||||
|
||||
@@ -456,6 +457,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
#if DEBUG
|
||||
WidgetGallery = _host.Services.GetRequiredService<Ui.Windows.WidgetGalleryWindow>();
|
||||
#endif
|
||||
InputBarLab = _host.Services.GetRequiredService<Ui.Windows.InputBarLabWindow>();
|
||||
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
|
||||
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
|
||||
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
|
||||
@@ -1042,6 +1044,14 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
WidgetGallery.Toggle();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (arg.Equals("lab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
InputBarLab.Toggle();
|
||||
return;
|
||||
}
|
||||
#if DEBUG
|
||||
#endif
|
||||
if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -379,6 +379,13 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
#endif
|
||||
// Temporary: a side-by-side comparison of input-row variants, so the
|
||||
// v1.14.0 style decision is made by looking rather than by imagining.
|
||||
// Deliberately NOT behind DEBUG -- the decision happens in the build Flo
|
||||
// actually runs. Comes out once the variants are picked.
|
||||
services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow(
|
||||
sp.GetRequiredService<Plugin>()
|
||||
));
|
||||
services.AddSingleton(sp => new DebuggerWindow(
|
||||
sp.GetRequiredService<Plugin>(),
|
||||
sp.GetRequiredService<PayloadHandler>()
|
||||
|
||||
@@ -67,6 +67,7 @@ internal sealed class PluginLifecycle : IAsyncDisposable
|
||||
#if DEBUG
|
||||
plugin.WindowSystem.AddWindow(plugin.WidgetGallery);
|
||||
#endif
|
||||
plugin.WindowSystem.AddWindow(plugin.InputBarLab);
|
||||
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
||||
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
||||
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
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 the input row, drawn side by side so a decision can be made by
|
||||
// looking rather than by imagining. Reachable with /hellion lab.
|
||||
//
|
||||
// Temporary. It comes out once the variants are picked, which is also why it is
|
||||
// not behind DEBUG -- the decision happens in the build that actually runs.
|
||||
//
|
||||
// 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;
|
||||
private int _fieldVariant;
|
||||
private int _pillVariant;
|
||||
private int _headerVariant;
|
||||
|
||||
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
|
||||
);
|
||||
dl.DrawGlowBorder(
|
||||
min,
|
||||
max,
|
||||
ColourUtil.ApplyAlpha(c.Accent, amount * 0.7f),
|
||||
1f,
|
||||
4
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
using (fonts.FontAwesome.Push())
|
||||
{
|
||||
var glyph = icon.ToIconString();
|
||||
var size = ImGui.CalcTextSize(glyph);
|
||||
var tint = ColourUtil.RgbaToAbgr(
|
||||
ColourUtil.EnsureContrast(c.TextPrimary, 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.RgbaToAbgr(ColourUtil.EnsureContrast(c.WindowBg, 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.RgbaToAbgr(ColourUtil.EnsureContrast(c.Accent, 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.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user