diff --git a/HellionChat/Ui/Components/Settings/ThemeMockup.cs b/HellionChat/Ui/Components/Settings/ThemeMockup.cs new file mode 100644 index 0000000..92c3d66 --- /dev/null +++ b/HellionChat/Ui/Components/Settings/ThemeMockup.cs @@ -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 + ); + } +} diff --git a/HellionChat/Ui/Components/Settings/ThemePicker.cs b/HellionChat/Ui/Components/Settings/ThemePicker.cs index 89a6407..eab7a23 100644 --- a/HellionChat/Ui/Components/Settings/ThemePicker.cs +++ b/HellionChat/Ui/Components/Settings/ThemePicker.cs @@ -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 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 Resolve(IEnumerable 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 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); } }