diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 1e01812..99aadc6 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -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(); #endif + InputBarLab = _host.Services.GetRequiredService(); DebuggerWindow = _host.Services.GetRequiredService(); FirstRunWizard = _host.Services.GetRequiredService(); ChannelPopoutPool = _host.Services.GetRequiredService(); @@ -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)) { diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index eee2b8d..a039011 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -379,6 +379,13 @@ internal static class PluginHostFactory sp.GetRequiredService() )); #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() + )); services.AddSingleton(sp => new DebuggerWindow( sp.GetRequiredService(), sp.GetRequiredService() diff --git a/HellionChat/PluginLifecycle.cs b/HellionChat/PluginLifecycle.cs index e0a27df..30c7966 100644 --- a/HellionChat/PluginLifecycle.cs +++ b/HellionChat/PluginLifecycle.cs @@ -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); diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs new file mode 100644 index 0000000..5f3fc07 --- /dev/null +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -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)); + } +}