feat(settings): add ThemePicker with five categories and switch lock
This commit is contained in:
@@ -147,6 +147,10 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
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(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user