feat(settings): add ThemePicker with five categories and switch lock

This commit is contained in:
2026-05-26 16:36:54 +02:00
parent a73cad1534
commit c6f7266194
2 changed files with 129 additions and 0 deletions
+4
View File
@@ -147,6 +147,10 @@ internal static class PluginHostFactory
sp.GetRequiredService<FontManager>() sp.GetRequiredService<FontManager>()
)); ));
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea()); services.AddSingleton(sp => new Ui.Components.Settings.ContentArea());
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
));
services.AddSingleton(sp => new Ui.Components.StatusBar( services.AddSingleton(sp => new Ui.Components.StatusBar(
sp.GetRequiredService<ThemeRegistry>(), sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>() sp.GetRequiredService<FontManager>()
@@ -0,0 +1,125 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Themes;
using HellionChat.Util;
namespace HellionChat.Ui.Components.Settings;
internal sealed class ThemePicker
{
private static readonly (string Category, string[] Slugs, bool DefaultExpanded)[] CategoryMap =
{
(
"Hellion Brand",
new[] { "hellion-arctic", "hellion-spectrum", "forge-merchantman" },
true
),
(
"Cool",
new[] { "night-blue", "event-horizon", "indigo-violet", "crystal-nocturne" },
false
),
("Natural", new[] { "mint-grove" }, false),
("Classic", new[] { "chat2-classic" }, false),
("Retro", new[] { "synthwave-sunset" }, false),
};
// T2 ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs
// to enforce coverage. Kept on the static map so the test does not pierce instance state.
internal static IEnumerable<string> CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs);
private readonly ThemeRegistry _themes;
private readonly Plugin _plugin;
public ThemePicker(ThemeRegistry themes, Plugin plugin)
{
_themes = themes;
_plugin = plugin;
}
public void Draw()
{
var locked = _themes.EditingThemeBuffer is not null;
using (ImRaii.Disabled(locked))
{
foreach (var (category, slugs, defaultExpanded) in CategoryMap)
{
var flags = defaultExpanded
? ImGuiTreeNodeFlags.DefaultOpen
: ImGuiTreeNodeFlags.None;
if (ImGui.CollapsingHeader(category, flags))
{
foreach (var slug in slugs)
{
DrawCard(slug);
}
}
}
}
if (locked && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.SetTooltip("Save or discard your edits first");
}
}
private void DrawCard(string slug)
{
if (!_themes.TryGet(slug, out var theme))
{
return;
}
var active = _themes.Active.Slug == slug;
var label = $"{theme.Name} — {theme.Author}##theme-card-{slug}";
// Selectable uses ImGui's default Header colour, not the theme's Surface.
// Swatch overlay below carries the theme cue; v1.7.x-polish if testers flag the mismatch.
if (ImGui.Selectable(label, active, ImGuiSelectableFlags.None, new Vector2(0, 40)))
{
_themes.Switch(slug);
Plugin.Config.Theme = slug;
_plugin.SaveConfig();
}
// Mini-Preview-Swatch overlay (3 ABGR boxes on the right side of the card).
// Selectable owns the hit-box; the DrawList overlay is decorative — full card area
// remains the click target, not just the swatch.
//
// RGBA-vs-ABGR-Disziplin: ThemeColors slots hold uint values in 0xRRGGBBAA layout
// (see ThemeColors.cs header comment). ImGui draw calls expect ABGR (native byte order)
// — pass theme colours through ColourUtil.RgbaToAbgr before any AddRectFilled / AddText
// / AddLine. The repo-wide pattern is "swap at the boundary" (see InputBar swap-at-
// boundary pattern). Forgetting the swap renders Red and Blue channels swapped and
// shifts the alpha byte into the green slot.
var draw = ImGui.GetWindowDrawList();
var max = ImGui.GetItemRectMax();
var min = ImGui.GetItemRectMin();
var swatchY = min.Y + 12;
DrawSwatch(
draw,
new Vector2(max.X - 60, swatchY),
ColourUtil.RgbaToAbgr(theme.Colors.Surface)
);
DrawSwatch(
draw,
new Vector2(max.X - 42, swatchY),
ColourUtil.RgbaToAbgr(theme.Colors.Primary)
);
DrawSwatch(
draw,
new Vector2(max.X - 24, swatchY),
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
);
}
// Caller contract: `colorAbgr` is already byte-swapped from the ThemeColors RGBA backing
// field via ColourUtil.RgbaToAbgr. Passing a raw RGBA value here renders with the wrong
// channel order.
private static void DrawSwatch(ImDrawListPtr draw, Vector2 topLeft, uint colorAbgr)
{
draw.AddRectFilled(topLeft, topLeft + new Vector2(14, 14), colorAbgr, 2f);
}
}