diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 3e05ec5..e729aaf 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -101,6 +101,13 @@ internal static class PluginHostFactory )); services.AddSingleton(_ => new Ui.StyleEngine.TokenResolver()); + + // Transient: each surface owns its motes, so two backdrops on screen do + // not drift in lockstep. + services.AddTransient(sp => new Ui.StyleEngine.SurfaceBackdrop( + sp.GetRequiredService(), + sp.GetRequiredService() + )); services.AddSingleton(sp => new Ui.StyleEngine.PushStack( sp.GetRequiredService() )); @@ -165,8 +172,7 @@ internal static class PluginHostFactory sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.ContentArea( - sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker( sp.GetRequiredService(), @@ -241,7 +247,8 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Integrations.FailedTellNotifier( sp.GetRequiredService>() diff --git a/HellionChat/Ui/Components/Settings/ContentArea.cs b/HellionChat/Ui/Components/Settings/ContentArea.cs index 5ccdd05..71e872a 100644 --- a/HellionChat/Ui/Components/Settings/ContentArea.cs +++ b/HellionChat/Ui/Components/Settings/ContentArea.cs @@ -1,28 +1,21 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; -using HellionChat.Themes; using HellionChat.Ui.StyleEngine; -using HellionChat.Util; namespace HellionChat.Ui.Components.Settings; internal sealed class ContentArea { - private readonly ThemeRegistry _themes; - private readonly TokenResolver _resolver; - private readonly AmbientParticles _motes = new(); + private readonly SurfaceBackdrop _backdrop; - public ContentArea(ThemeRegistry themes, TokenResolver resolver) + public ContentArea(SurfaceBackdrop backdrop) { - _themes = themes; - _resolver = resolver; + _backdrop = backdrop; } public void Draw(string activeTab, Action renderTab) { - var colors = _themes.Active.Colors; - // Transparent child with the ground painted by hand. ChildBg takes one // flat colour and nothing else, so a gradient, an accent wash or motes // are all impossible through it -- and the pane had no ground at all @@ -34,30 +27,7 @@ internal sealed class ContentArea return; } - // GetWindowDrawList inside a child is that child's own list, so all of - // this lands behind its content without a channel split. - var dl = ImGui.GetWindowDrawList(); - var min = ImGui.GetWindowPos(); - var max = min + ImGui.GetWindowSize(); - var surface = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.SurfaceBase, colors)); - var accent = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.AccentPrimary, colors)); - - dl.DrawVerticalGradient(min, max, surface, topLift: 0.20f, bottomDrop: 0.14f); - - // A wash of the accent down from the top edge, fading to nothing by - // roughly a third of the height. Without it the pane is a neutral box - // that happens to sit under a themed window; with it the theme reaches - // the surface the user is actually reading. - dl.AddRectFilledMultiColor( - min, - new Vector2(max.X, min.Y + (max.Y - min.Y) * 0.38f), - ColourUtil.ApplyAlpha(accent, 0.10f), - ColourUtil.ApplyAlpha(accent, 0.05f), - accent & 0x00FFFFFFu, - accent & 0x00FFFFFFu - ); - - _motes.Draw(dl, min, max, accent); + _backdrop.Draw(); renderTab(activeTab); } diff --git a/HellionChat/Ui/StyleEngine/AmbientParticles.cs b/HellionChat/Ui/StyleEngine/AmbientParticles.cs index c42f21e..72a5805 100644 --- a/HellionChat/Ui/StyleEngine/AmbientParticles.cs +++ b/HellionChat/Ui/StyleEngine/AmbientParticles.cs @@ -27,7 +27,7 @@ internal sealed class AmbientParticles private readonly float[] _y; private int _lastFrame = -1; - internal AmbientParticles(int count = 24, int seed = 0x48454C4C) + internal AmbientParticles(int count = 70, int seed = 0x48454C4C) { // Seeded rather than time-based: the layout is identical every session, // so a screenshot taken today matches one taken tomorrow. @@ -40,8 +40,8 @@ internal sealed class AmbientParticles _motes[i] = new Mote { X = (float)rng.NextDouble(), - Speed = 0.012f + (float)rng.NextDouble() * 0.022f, - Radius = 1f + (float)rng.NextDouble() * 1.6f, + Speed = 0.014f + (float)rng.NextDouble() * 0.030f, + Radius = 1.1f + (float)rng.NextDouble() * 2.6f, Phase = (float)rng.NextDouble() * MathF.Tau, Sway = 0.004f + (float)rng.NextDouble() * 0.010f, }; @@ -90,12 +90,22 @@ internal sealed class AmbientParticles // instead of popping at the edge. var edge = Math.Clamp(MathF.Min(_y[i], 1f - _y[i]) * 6f, 0f, 1f); var pulse = 0.55f + 0.45f * MathF.Sin(time * 0.6f + m.Phase); + var radius = m.Radius * Metrics.Scale; + // Halo first, core on top. A flat disc at this size reads as a + // speck of dirt on the screen; the soft ring around it is what makes + // it look lit. dl.AddCircleFilled( pos, - m.Radius * Metrics.Scale, - ColourUtil.ApplyAlpha(accentAbgr, 0.16f * edge * pulse), - 8 + radius * 2.1f, + ColourUtil.ApplyAlpha(accentAbgr, 0.13f * edge * pulse), + 10 + ); + dl.AddCircleFilled( + pos, + radius, + ColourUtil.ApplyAlpha(accentAbgr, 0.42f * edge * pulse), + 10 ); } } diff --git a/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs b/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs new file mode 100644 index 0000000..d724aa5 --- /dev/null +++ b/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs @@ -0,0 +1,59 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.StyleEngine; + +// The ground under a scrolling surface: gradient, accent wash, drifting motes. +// Pulled out of the settings pane so the chat log can stand on the same floor -- +// two windows in one plugin looking like two different products was half of why +// the settings window read as untouched. +// +// Each instance owns its motes, so two surfaces on screen at once do not share +// one drift pattern. +internal sealed class SurfaceBackdrop +{ + private readonly ThemeRegistry _themes; + private readonly TokenResolver _resolver; + private readonly AmbientParticles _motes; + + internal SurfaceBackdrop(ThemeRegistry themes, TokenResolver resolver, int moteCount = 70) + { + _themes = themes; + _resolver = resolver; + _motes = new AmbientParticles(moteCount); + } + + // Call right after entering a child, before its content. GetWindowDrawList + // is then that child's own list, so everything lands behind the content + // without a channel split. + internal void Draw(float accentWashHeight = 0.38f, float darken = 0f, bool motes = true) + { + var dl = ImGui.GetWindowDrawList(); + var min = ImGui.GetWindowPos(); + var max = min + ImGui.GetWindowSize(); + var colors = _themes.Active.Colors; + + var surface = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.SurfaceBase, colors)); + if (darken > 0f) + surface = ColourUtil.LerpTowardBlack(surface, darken); + + var accent = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.AccentPrimary, colors)); + + dl.DrawVerticalGradient(min, max, surface, topLift: 0.20f, bottomDrop: 0.14f); + + if (accentWashHeight > 0f) + dl.AddRectFilledMultiColor( + min, + new Vector2(max.X, min.Y + (max.Y - min.Y) * accentWashHeight), + ColourUtil.ApplyAlpha(accent, 0.10f), + ColourUtil.ApplyAlpha(accent, 0.05f), + accent & 0x00FFFFFFu, + accent & 0x00FFFFFFu + ); + + if (motes) + _motes.Draw(dl, min, max, accent); + } +} diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index a65460e..8c3de06 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -31,6 +31,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow private readonly Components.StatusBar _status; private readonly Lender _handlerLender; private readonly ChannelPopoutPool _pool; + private readonly Ui.StyleEngine.SurfaceBackdrop _backdrop; private Tab? _activeTab; @@ -53,7 +54,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow Components.InputBar input, Components.StatusBar status, Lender handlerLender, - ChannelPopoutPool pool + ChannelPopoutPool pool, + Ui.StyleEngine.SurfaceBackdrop backdrop ) : base($"{Plugin.PluginName}###hellion-main") { @@ -65,6 +67,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow _status = status; _handlerLender = handlerLender; _pool = pool; + _backdrop = backdrop; Size = new Vector2(DefaultWidth, DefaultHeight); SizeCondition = ImGuiCond.FirstUseEver; @@ -380,8 +383,16 @@ internal sealed class MainWindow : Window, IFocusableChatWindow ) ) { - if (messages.Success && _activeTab is not null) - _messages.Draw(_activeTab); + if (messages.Success) + { + // No accent wash here. It works on a settings pane, where the + // top of the surface is a heading; over a chat log the first + // messages would sit in a tinted band and read as highlighted. + _backdrop.Draw(accentWashHeight: 0f); + + if (_activeTab is not null) + _messages.Draw(_activeTab); + } } // Inside-mode inline render: measure first so PreviewHeight is fresh