feat(themes): restore the per-card theme preview mockup in the picker
This commit is contained in:
@@ -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.
|
// 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);
|
internal static IEnumerable<string> CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs);
|
||||||
|
|
||||||
|
private const float CardHeight = 132f;
|
||||||
|
|
||||||
private readonly ThemeRegistry _themes;
|
private readonly ThemeRegistry _themes;
|
||||||
private readonly Plugin _plugin;
|
private readonly Plugin _plugin;
|
||||||
|
|
||||||
@@ -51,10 +53,7 @@ internal sealed class ThemePicker
|
|||||||
: ImGuiTreeNodeFlags.None;
|
: ImGuiTreeNodeFlags.None;
|
||||||
if (ImGui.CollapsingHeader(category, flags))
|
if (ImGui.CollapsingHeader(category, flags))
|
||||||
{
|
{
|
||||||
foreach (var slug in slugs)
|
DrawThemeGrid(Resolve(slugs));
|
||||||
{
|
|
||||||
DrawCard(slug);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +69,7 @@ internal sealed class ThemePicker
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
foreach (var theme in customs)
|
DrawThemeGrid(customs);
|
||||||
{
|
|
||||||
DrawCard(theme.Slug);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
return;
|
yield return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
var active = _themes.Active.Slug == slug;
|
// Grid of theme cards, each carrying a mini chat mockup (restored from 1.5.6
|
||||||
var label = $"{theme.Name} — {theme.Author}##theme-card-{slug}";
|
// DrawThemeGrid + ThemeMockup). Column count adapts to the available width.
|
||||||
|
private void DrawThemeGrid(IEnumerable<Theme> themes)
|
||||||
// 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);
|
var list = themes.ToList();
|
||||||
Plugin.Config.Theme = slug;
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeMockup.Draw(origin + new Vector2(12f, 12f), new Vector2(w - 24f, 60f), theme);
|
||||||
|
|
||||||
|
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(theme.Slug);
|
||||||
|
Plugin.Config.Theme = theme.Slug;
|
||||||
_plugin.SaveConfig();
|
_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