feat(style): put the chat log on the same floor as the settings
The motes were too subtle to register, so: seventy instead of twenty-four, wider radius range, and each one is a soft halo with a brighter core rather than a flat disc. At this size a single filled circle reads as a speck of dirt on the screen; the ring around it is what makes it look lit. The backdrop moves into its own component and the chat log gets it too. Two windows in one plugin looking like two separate products was half the reason the settings window read as untouched -- the chat had been reworked in v1.10.0 and the settings had not, and no amount of work inside one window closes that gap. No accent wash over the message list. It works on a settings pane, where the top of the surface is a heading; over a chat log the oldest visible messages would sit in a tinted band and read as highlighted. Registered transient rather than singleton, so two surfaces on screen own separate mote sets instead of sharing one drift pattern -- which would be visible the moment a popout sits next to the main window. MainWindow's constructor changed, so this needs the DI smoke pass.
This commit is contained in:
@@ -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<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.StyleEngine.PushStack(
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
@@ -165,8 +172,7 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
@@ -241,7 +247,8 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||
sp.GetRequiredService<Ui.Components.StatusBar>(),
|
||||
sp.GetRequiredService<Lender<PayloadHandler>>(),
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
|
||||
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||
|
||||
@@ -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<string> 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
||||
private readonly Components.StatusBar _status;
|
||||
private readonly Lender<PayloadHandler> _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<PayloadHandler> 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
|
||||
|
||||
Reference in New Issue
Block a user