feat(themes): restore the per-card theme preview mockup in the picker

This commit is contained in:
2026-06-15 18:49:45 +02:00
parent 18834cddae
commit 8cbf9c554f
2 changed files with 170 additions and 56 deletions
@@ -0,0 +1,82 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using HellionChat.Themes;
using HellionChat.Util;
namespace HellionChat.Ui.Components.Settings;
// Mini chat-window mockup drawn straight into the WindowDrawList (restored from
// 1.5.6 ThemeMockup). No textures, no per-frame allocations — pure rect/text.
internal static class ThemeMockup
{
public static void Draw(Vector2 origin, Vector2 size, Theme theme)
{
var draw = ImGui.GetWindowDrawList();
var c = theme.Colors;
draw.AddRectFilled(
origin,
origin + size,
ColourUtil.RgbaToAbgr(c.WindowBg | 0xFFu),
theme.Layout.WindowRounding
);
var titleHeight = 14f;
draw.AddRectFilled(
origin,
new Vector2(origin.X + size.X, origin.Y + titleHeight),
ColourUtil.RgbaToAbgr(c.Identity),
theme.Layout.WindowRounding
);
var tabY = origin.Y + titleHeight + 4f;
var tabHeight = 12f;
for (var i = 0; i < 3; i++)
{
var tabX = origin.X + 6f + i * 28f;
var color = i == 0 ? c.FrameBg : c.ChildBg;
draw.AddRectFilled(
new Vector2(tabX, tabY),
new Vector2(tabX + 26f, tabY + tabHeight),
ColourUtil.RgbaToAbgr(color),
theme.Layout.TabRounding
);
if (i == 0)
{
draw.AddRectFilled(
new Vector2(tabX, tabY + tabHeight - 2f),
new Vector2(tabX + 26f, tabY + tabHeight),
ColourUtil.RgbaToAbgr(c.Primary)
);
}
}
var rowY = tabY + tabHeight + 6f;
var rowHeight = 18f;
draw.AddRectFilled(
new Vector2(origin.X + 6f, rowY),
new Vector2(origin.X + size.X - 6f, rowY + rowHeight),
ColourUtil.RgbaToAbgr(c.Surface),
2f
);
var btnW = 28f;
var btnH = 10f;
var btnX = origin.X + size.X - btnW - 6f;
var btnY = origin.Y + size.Y - btnH - 6f;
draw.AddRectFilled(
new Vector2(btnX, btnY),
new Vector2(btnX + btnW, btnY + btnH),
ColourUtil.RgbaToAbgr(c.Accent),
theme.Layout.FrameRounding
);
draw.AddRect(
origin,
origin + size,
ColourUtil.RgbaToAbgr(c.Border),
theme.Layout.WindowRounding
);
}
}
@@ -29,6 +29,8 @@ internal sealed class ThemePicker
// 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 const float CardHeight = 132f;
private readonly ThemeRegistry _themes;
private readonly Plugin _plugin;
@@ -51,10 +53,7 @@ internal sealed class ThemePicker
: ImGuiTreeNodeFlags.None;
if (ImGui.CollapsingHeader(category, flags))
{
foreach (var slug in slugs)
{
DrawCard(slug);
}
DrawThemeGrid(Resolve(slugs));
}
}
@@ -70,10 +69,7 @@ internal sealed class ThemePicker
)
)
{
foreach (var theme in customs)
{
DrawCard(theme.Slug);
}
DrawThemeGrid(customs);
}
}
}
@@ -84,61 +80,97 @@ internal sealed class ThemePicker
}
}
private void DrawCard(string slug)
private IEnumerable<Theme> Resolve(IEnumerable<string> slugs)
{
if (!_themes.TryGet(slug, out var theme))
{
foreach (var slug in slugs)
if (_themes.TryGet(slug, out var theme))
yield return theme;
}
// Grid of theme cards, each carrying a mini chat mockup (restored from 1.5.6
// DrawThemeGrid + ThemeMockup). Column count adapts to the available width.
private void DrawThemeGrid(IEnumerable<Theme> themes)
{
var list = themes.ToList();
if (list.Count == 0)
return;
var avail = ImGui.GetContentRegionAvail().X;
var columns = avail >= 460f ? 2 : 1;
var cardWidth = columns > 1 ? (avail - (columns - 1) * 8f) / columns : avail;
for (var i = 0; i < list.Count; i++)
{
DrawThemeCard(list[i], cardWidth, CardHeight);
if ((i + 1) % columns != 0 && i != list.Count - 1)
ImGui.SameLine();
}
}
private void DrawThemeCard(Theme theme, float w, float h)
{
ImGui.BeginGroup();
var isActive = string.Equals(
theme.Slug,
_themes.Active.Slug,
StringComparison.OrdinalIgnoreCase
);
var origin = ImGui.GetCursorScreenPos();
var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h));
var hovered = ImGui.IsItemHovered();
var draw = ImGui.GetWindowDrawList();
draw.AddRectFilled(
origin,
origin + new Vector2(w, h),
ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu),
4f
);
if (isActive)
{
draw.AddRect(
origin,
origin + new Vector2(w, h),
ColourUtil.RgbaToAbgr(theme.Colors.Primary),
4f,
ImDrawFlags.None,
2f
);
}
else if (hovered)
{
draw.AddRect(
origin,
origin + new Vector2(w, h),
ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u),
4f,
ImDrawFlags.None,
1f
);
}
var active = _themes.Active.Slug == slug;
var label = $"{theme.Name} — {theme.Author}##theme-card-{slug}";
ThemeMockup.Draw(origin + new Vector2(12f, 12f), new Vector2(w - 24f, 60f), theme);
// 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)))
draw.AddText(
origin + new Vector2(12f, 80f),
ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary),
theme.Name
);
draw.AddText(
origin + new Vector2(12f, 100f),
ColourUtil.RgbaToAbgr(theme.Colors.TextMuted),
theme.Author
);
ImGui.EndGroup();
if (clicked)
{
_themes.Switch(slug);
Plugin.Config.Theme = slug;
_themes.Switch(theme.Slug);
Plugin.Config.Theme = 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);
}
}