feat(themes): wire theme engine into plugin draw pipeline + migrate v13→v14

HellionStyle.PushGlobal nimmt jetzt eine Theme-Instance + Window-Opacity
und liest alle Color- und Style-Slots aus dem aktiven Theme statt aus
einer fixen Konstanten-Tabelle. Plugin hält die ThemeRegistry und schaltet
beim Init auf das in Config.Theme gespeicherte Slug.

Configuration v13 → v14:
- Neue Felder Theme (slug), WindowOpacity, ReduceMotion, UseCompactDensity,
  ShowThemeQuickPicker
- HellionThemeEnabled und HellionThemeWindowOpacity sind ab v14 [Obsolete]
  und bleiben bis v1.2.0 als JSON-Safety-Net erhalten
- Migration setzt alle Bestandsuser auf hellion-arctic; chat2-classic
  bleibt im Themes-Tab als Upstream-Look wählbar
- WindowOpacity übernimmt den Wert von HellionThemeWindowOpacity, alte
  HellionThemeEnabled-Flag entfällt funktional (Theme-Engine ist immer aktiv)

Konsumenten der alten Felder (ChatLogWindow.BgAlpha, Popout.BgAlpha) lesen
jetzt das neue WindowOpacity. Die Settings-UI in Appearance.cs schreibt
übergangsweise weiter in die Obsolete-Felder; Phase J ersetzt diesen Block
durch den dedizierten Themes-Tab. CS0612/CS0618 sind dort gezielt mit
pragma gekapselt.
This commit is contained in:
2026-05-05 13:51:31 +02:00
parent 4bf6c3ef1f
commit dd3a0ea069
6 changed files with 153 additions and 187 deletions
+86 -173
View File
@@ -1,3 +1,4 @@
using HellionChat.Themes;
using HellionChat.Util;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
@@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii;
namespace HellionChat.Ui;
/// <summary>
/// ImGui style override for Hellion Chat. Industrial HUD palette with three
/// distinct accents — cyan-teal as the primary action color, industrial
/// amber for active state highlights, slate-violet for title bars and
/// active tabs — on a deep-slate frame background with steel borders.
///
/// Two entry points:
/// Push — local color stack, scoped via using-block. Use inside
/// Hellion-only surfaces (Privacy tab, first-run wizard).
/// PushGlobal — full color + style variable stack. Pushed once per frame
/// in Plugin.Draw so every Hellion-rendered window inherits
/// the look. Cheap to pop because ImGui keeps its own stack.
/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine
/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window-
/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme
/// gelesen statt aus einer fixen Konstanten-Tabelle.
/// </summary>
internal static class HellionStyle
{
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
// expects. Hex values are sourced from the Hellion Online Media brand
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
// Primary — Arctic Cyan, used for every interactive control (buttons,
// checks, sliders, separators when hovered). Three brand stages plus a
// hover that lifts to brand-color-light and a press that drops to
// brand-color-dark.
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
// Identity — brand-color-dark teal for window title bars and the
// active tab. Sits visibly below the primary cyan on buttons so the
// user sees "where am I" (deep teal) versus "what can I click"
// (brand cyan) without leaving the cyan family.
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
// Accent — Ember Orange for warm highlights on grips and scrollbar
// pulls. Replaces the previous industrial amber so the plugin matches
// the website's CTA palette. AccentActive is reserved for any future
// pressed-state on accent surfaces; the current slots only need
// AccentRgba and AccentHoverRgba.
private const uint AccentRgba = 0xF97316FF; // accent-color
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
// Surfaces — Hellion brand background ladder. Window darkest, frame
// hover ladder climbs into surface tones. Matches the website's
// background / background-medium / background-light / surface vars.
private const uint WindowBgRgba = 0x070B12FF; // background
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
private const uint FrameBgRgba = 0x141E30FF; // background-light
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
private const uint BorderRgba = 0x00BED266;
private const uint BorderShadowRgba = 0x00000000;
// Headers / collapsing-headers / tree nodes / selectables — same
// surface ladder as frames so panels feel consistent.
private const uint HeaderRgba = 0x141E30FF;
private const uint HeaderHoverRgba = 0x1A2538FF;
private const uint HeaderActiveRgba = 0x22303FFF;
// Title bars — Identity teal on active so the focused window reads
// as "yours" without using accent or primary slots.
private const uint TitleBgRgba = 0x070B12FF;
private const uint TitleBgActiveRgba = IdentityRgba;
private const uint TitleBgCollapsedRgba = 0x05080EFF;
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
// active. Unfocused-active uses the deeper Identity stage so an
// unfocused window's active tab still reads but does not pull focus.
private const uint TabRgba = 0x141E30FF;
private const uint TabHoveredRgba = IdentityHoverRgba;
private const uint TabActiveRgba = IdentityRgba;
private const uint TabUnfocusedRgba = 0x0C1220FF;
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
// Scrollbar — Ember on grab so the pull stands out without competing
// with the cyan action buttons. Idle grab is a subtle surface tone,
// hover/active climb into accent.
private const uint ScrollbarBgRgba = 0x070B12FF;
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
private const uint ScrollbarGrabActiveRgba = AccentRgba;
// Resize grip — same Ember treatment as the scrollbar.
private const uint ResizeGripRgba = 0x141E30FF;
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
private const uint ResizeGripActiveRgba = AccentRgba;
// Separator and check mark / slider follow the primary cyan.
/// <summary>
/// Local color stack for Hellion-only surfaces. Cheap. Use inside a
/// `using var _ = HellionStyle.Push();` block.
/// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a
/// `using var _ = HellionStyle.Push(theme);` block.
/// </summary>
internal static IDisposable Push()
internal static IDisposable Push(Theme theme)
{
var c = theme.Colors;
var stack = new StackHandle();
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
stack.PushColor(ImGuiCol.Border, BorderRgba);
stack.PushColor(ImGuiCol.Header, HeaderRgba);
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.Button, c.Primary);
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
stack.PushColor(ImGuiCol.Border, c.Border);
stack.PushColor(ImGuiCol.Header, c.Surface);
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
return stack;
}
/// <summary>
/// Global color and style-variable stack pushed once per frame in
/// Plugin.Draw. Covers every ImGui surface the plugin renders so the
/// Hellion look is consistent across upstream and Hellion tabs.
/// Plugin.Draw. Drives every Hellion-rendered window from the active
/// theme's palette and layout values.
/// </summary>
/// <param name="windowOpacity">Window background alpha (0.51.0). Lower
/// values let the game shine through the plugin panes.</param>
internal static IDisposable PushGlobal(float windowOpacity = 1.0f)
/// <param name="theme">Active theme from ThemeRegistry.</param>
/// <param name="windowOpacity">Window background alpha (0.51.0).</param>
internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f)
{
var c = theme.Colors;
var l = theme.Layout;
var stack = new StackHandle();
// Mix the configured opacity into both the outer window and the
// inner content child backgrounds — without ChildBg following the
// slider the chat log stays opaque inside even when the user
// wants to see the game behind it during combat. Form fields and
// popups (FrameBg, PopupBg) still stay opaque so input is readable.
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte;
var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte;
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte;
// Layout — geometric edges, modest rounding, single-pixel borders.
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f);
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f);
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f);
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f);
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
// Layout
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding);
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
// Surfaces.
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba);
stack.PushColor(ImGuiCol.Border, BorderRgba);
stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba);
// Surfaces
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
stack.PushColor(ImGuiCol.PopupBg, c.ChildBg);
stack.PushColor(ImGuiCol.Border, c.Border);
stack.PushColor(ImGuiCol.BorderShadow, 0u);
// Frames (input fields, combos, sliders).
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
// Frames
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
// Title bars — tertiary identity on active.
stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba);
stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba);
stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba);
// Title bars
stack.PushColor(ImGuiCol.TitleBg, c.WindowBg);
stack.PushColor(ImGuiCol.TitleBgActive, c.Identity);
stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg);
// Buttons — primary cyan.
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
// Buttons
stack.PushColor(ImGuiCol.Button, c.Primary);
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
// Headers / selectables — slate with subtle steps.
stack.PushColor(ImGuiCol.Header, HeaderRgba);
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
// Headers / selectables
stack.PushColor(ImGuiCol.Header, c.Surface);
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
// Tabs — tertiary identity for the active tab.
stack.PushColor(ImGuiCol.Tab, TabRgba);
stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba);
stack.PushColor(ImGuiCol.TabActive, TabActiveRgba);
stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba);
stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba);
// Tabs
stack.PushColor(ImGuiCol.Tab, c.FrameBg);
stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight);
stack.PushColor(ImGuiCol.TabActive, c.Identity);
stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg);
stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark);
// Scrollbar.
stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba);
stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba);
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba);
stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba);
// Scrollbar
stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg);
stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface);
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight);
stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent);
// Resize grip — secondary amber on active.
stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba);
stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba);
stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba);
// Resize grip
stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg);
stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight);
stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent);
// Check mark + slider grab — primary cyan.
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
// Check mark + slider grab
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
// Separator — primary cyan when hovered/active so the eye
// immediately sees that splitters are interactive.
stack.PushColor(ImGuiCol.Separator, BorderRgba);
stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba);
// Separator
stack.PushColor(ImGuiCol.Separator, c.Border);
stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight);
stack.PushColor(ImGuiCol.SeparatorActive, c.Primary);
return stack;
}