From 2a9b5812edd39005ece2e2ffbe43b3d1e80f99c9 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:29:44 +0900 Subject: [PATCH] add theme override customizations --- .../Configurations/UiStyleOverride.cs | 21 + .../Configurations/UiThemeConfig.cs | 12 + .../UiThemeConfigService.cs | 14 + LightlessSync/Plugin.cs | 5 +- LightlessSync/UI/SettingsUi.cs | 254 +++++++++++- LightlessSync/UI/Style/MainStyle.cs | 382 ++++++++++-------- 6 files changed, 526 insertions(+), 162 deletions(-) create mode 100644 LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs create mode 100644 LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs create mode 100644 LightlessSync/LightlessConfiguration/UiThemeConfigService.cs diff --git a/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs b/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs new file mode 100644 index 0000000..3ea43eb --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/UiStyleOverride.cs @@ -0,0 +1,21 @@ +using System; +using System.Numerics; + +namespace LightlessSync.LightlessConfiguration.Configurations; + +[Serializable] +public class UiStyleOverride +{ + public uint? Color { get; set; } + public float? Float { get; set; } + public Vector2Config? Vector2 { get; set; } + + public bool IsEmpty => Color is null && Float is null && Vector2 is null; +} + +[Serializable] +public record struct Vector2Config(float X, float Y) +{ + public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y); + public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y); +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs new file mode 100644 index 0000000..aa0b219 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/UiThemeConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace LightlessSync.LightlessConfiguration.Configurations; + +[Serializable] +public class UiThemeConfig : ILightlessConfiguration +{ + public Dictionary StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase); + + public int Version { get; set; } = 1; +} diff --git a/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs b/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs new file mode 100644 index 0000000..21a5051 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/UiThemeConfigService.cs @@ -0,0 +1,14 @@ +using LightlessSync.LightlessConfiguration.Configurations; + +namespace LightlessSync.LightlessConfiguration; + +public class UiThemeConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "ui-theme.json"; + + public UiThemeConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 9a397be..7a54ce9 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -192,10 +192,12 @@ public sealed class Plugin : IDalamudPlugin httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); return httpClient; }); + collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => { var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); - LightlessSync.UI.Style.MainStyle.Init(cfg); + var theme = s.GetRequiredService(); + LightlessSync.UI.Style.MainStyle.Init(cfg, theme); return cfg; }); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -207,6 +209,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 5b2ce6d..de20fb6 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -18,6 +18,7 @@ using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.UtilsEnum.Enum; using LightlessSync.WebAPI; @@ -44,6 +45,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly CacheMonitor _cacheMonitor; private readonly LightlessConfigService _configService; + private readonly UiThemeConfigService _themeConfigService; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DalamudUtilService _dalamudUtilService; private readonly HttpClient _httpClient; @@ -94,7 +96,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _wasOpen = false; public SettingsUi(ILogger logger, - UiSharedService uiShared, LightlessConfigService configService, + UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService, PairManager pairManager, ServerConfigurationManager serverConfigurationManager, PlayerPerformanceConfigService playerPerformanceConfigService, @@ -110,6 +112,7 @@ public class SettingsUi : WindowMediatorSubscriberBase NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; + _themeConfigService = themeConfigService; _pairManager = pairManager; _serverConfigurationManager = serverConfigurationManager; _playerPerformanceConfigService = playerPerformanceConfigService; @@ -241,6 +244,253 @@ public class SettingsUi : WindowMediatorSubscriberBase return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); } + private static Vector4 PackedThemeColorToVector4(uint packed) + => new( + (packed & 0xFF) / 255f, + ((packed >> 8) & 0xFF) / 255f, + ((packed >> 16) & 0xFF) / 255f, + ((packed >> 24) & 0xFF) / 255f); + + private static uint ThemeVector4ToPackedColor(Vector4 color) + { + static byte ToByte(float channel) + { + var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); + return (byte)Math.Clamp((int)scaled, 0, 255); + } + + var r = ToByte(color.X); + var g = ToByte(color.Y); + var b = ToByte(color.Z); + var a = ToByte(color.W); + return (uint)(r | (g << 8) | (b << 16) | (a << 24)); + } + + private void UpdateStyleOverride(string key, Action updater) + { + var overrides = _themeConfigService.Current.StyleOverrides; + + if (!overrides.TryGetValue(key, out var entry)) + entry = new UiStyleOverride(); + + updater(entry); + + if (entry.IsEmpty) + overrides.Remove(key); + else + overrides[key] = entry; + + _themeConfigService.Save(); + } + + private void DrawThemeOverridesSection() + { + ImGui.TextUnformatted("Lightless Theme Overrides"); + _uiShared.DrawHelpText("Adjust the Lightless redesign theme. Overrides only apply when the redesign is enabled."); + + if (!_configService.Current.UseLightlessRedesign) + UiSharedService.ColorTextWrapped("The Lightless redesign is currently disabled. Enable it to see these changes take effect.", UIColors.Get("DimRed")); + + const ImGuiTableFlags flags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp; + if (!ImGui.BeginTable("##ThemeOverridesTable", 3, flags)) + return; + + ImGui.TableSetupColumn("Element", ImGuiTableColumnFlags.WidthFixed, 325f); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 70f); + ImGui.TableHeadersRow(); + + DrawThemeCategoryRow("Colors"); + foreach (var option in MainStyle.ColorOptions) + DrawThemeColorRow(option); + + DrawThemeCategoryRow("Spacing & Padding"); + foreach (var option in MainStyle.Vector2Options) + DrawThemeVectorRow(option); + + DrawThemeCategoryRow("Rounding & Sizes"); + foreach (var option in MainStyle.FloatOptions) + DrawThemeFloatRow(option); + + ImGui.EndTable(); + } + + private static void DrawThemeCategoryRow(string label) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextColored(UIColors.Get("LightlessPurple"), label); + ImGui.TableSetColumnIndex(1); + ImGui.TableSetColumnIndex(2); + } + + private void DrawThemeColorRow(MainStyle.StyleColorOption option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + bool showTooltip = ImGui.IsItemHovered(); + + var tooltip = string.Empty; + if (!string.IsNullOrEmpty(option.Description)) + tooltip = option.Description; + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + if (!string.IsNullOrEmpty(option.UiColorKey)) + { + if (!string.IsNullOrEmpty(tooltip)) + tooltip += "\n"; + tooltip += $"Default uses UIColors[\"{option.UiColorKey}\"]"; + + ImGui.SameLine(); + ImGui.TextDisabled($"(UIColors.{option.UiColorKey})"); + if (ImGui.IsItemHovered()) + showTooltip = true; + } + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Color is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Color = null; + entry.Float = null; + entry.Vector2 = null; + }); + + existing = null; + } + + if (showTooltip && !string.IsNullOrEmpty(tooltip)) + ImGui.SetTooltip(tooltip); + + var defaultColor = MainStyle.NormalizeColorVector(option.DefaultValue()); + var current = existing?.Color is { } packed ? PackedThemeColorToVector4(packed) : defaultColor; + var edit = current; + + ImGui.TableSetColumnIndex(1); + if (ImGui.ColorEdit4($"##theme-color-{option.Key}", ref edit, ImGuiColorEditFlags.AlphaPreviewHalf)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Color = ThemeVector4ToPackedColor(edit); + entry.Float = null; + entry.Vector2 = null; + }); + } + } + + private void DrawThemeVectorRow(MainStyle.StyleVector2Option option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + if (!string.IsNullOrEmpty(option.Description) && ImGui.IsItemHovered()) + ImGui.SetTooltip(option.Description); + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Vector2 is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Vector2 = null; + entry.Color = null; + entry.Float = null; + }); + existing = null; + } + + var defaultValue = option.DefaultValue(); + var current = existing?.Vector2 is { } vectorOverride ? (Vector2)vectorOverride : defaultValue; + var edit = current; + + ImGui.TableSetColumnIndex(1); + if (ImGui.DragFloat2($"##theme-vector-{option.Key}", ref edit, option.Speed)) + { + if (option.Min is { } min) + edit = Vector2.Max(edit, min); + if (option.Max is { } max) + edit = Vector2.Min(edit, max); + + UpdateStyleOverride(option.Key, entry => + { + entry.Vector2 = new Vector2Config(edit.X, edit.Y); + entry.Color = null; + entry.Float = null; + }); + } + } + + private void DrawThemeFloatRow(MainStyle.StyleFloatOption option) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.TextUnformatted(option.Label); + if (!string.IsNullOrEmpty(option.Description) && ImGui.IsItemHovered()) + ImGui.SetTooltip(option.Description); + + var overrides = _themeConfigService.Current.StyleOverrides; + overrides.TryGetValue(option.Key, out var existing); + + ImGui.TableSetColumnIndex(2); + if (DrawStyleResetButton(option.Key, existing?.Float is not null)) + { + UpdateStyleOverride(option.Key, entry => + { + entry.Float = null; + entry.Color = null; + entry.Vector2 = null; + }); + existing = null; + } + + var current = existing?.Float ?? option.DefaultValue; + var edit = current; + + var min = option.Min ?? float.MinValue; + var max = option.Max ?? float.MaxValue; + + ImGui.TableSetColumnIndex(1); + if (ImGui.DragFloat($"##theme-float-{option.Key}", ref edit, option.Speed, min, max, "%.2f")) + { + if (option.Min.HasValue) + edit = MathF.Max(option.Min.Value, edit); + if (option.Max.HasValue) + edit = MathF.Min(option.Max.Value, edit); + + UpdateStyleOverride(option.Key, entry => + { + entry.Float = edit; + entry.Color = null; + entry.Vector2 = null; + }); + } + } + + private bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) + { + using var id = ImRaii.PushId($"reset-{key}"); + using var disabled = ImRaii.Disabled(!hasOverride); + var availableWidth = ImGui.GetContentRegionAvail().X; + bool pressed = false; + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + pressed = true; + } + + var tooltip = tooltipOverride ?? (hasOverride + ? "Reset this style override to its default value." + : "Value already matches the default."); + UiSharedService.AttachToolTip(tooltip); + return pressed; + } + private void DrawBlockedTransfers() { _lastTab = "BlockedTransfers"; @@ -1642,6 +1892,8 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + DrawThemeOverridesSection(); + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } diff --git a/LightlessSync/UI/Style/MainStyle.cs b/LightlessSync/UI/Style/MainStyle.cs index d40ed2e..d3d8b68 100644 --- a/LightlessSync/UI/Style/MainStyle.cs +++ b/LightlessSync/UI/Style/MainStyle.cs @@ -1,169 +1,231 @@ -// inspiration: brio because it's style is fucking amazing +// inspiration: brio because it's style is fucking amazing using Dalamud.Bindings.ImGui; using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Configurations; +using System; +using System.Collections.Generic; using System.Numerics; -namespace LightlessSync.UI.Style +namespace LightlessSync.UI.Style; + +internal static class MainStyle { - internal static class MainStyle + public readonly record struct StyleColorOption(string Key, string Label, Func DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null); + public readonly record struct StyleFloatOption(string Key, string Label, float DefaultValue, ImGuiStyleVar Target, float? Min = null, float? Max = null, float Speed = 0.25f, string? Description = null); + public readonly record struct StyleVector2Option(string Key, string Label, Func DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null); + + private static LightlessConfigService? _config; + private static UiThemeConfigService? _themeConfig; + public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig) { - private static LightlessConfigService? _config; - public static void Init(LightlessConfigService config) => _config = config; - public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false; - - private static bool _hasPushed; - private static int _pushedColorCount; - private static int _pushedStyleVarCount; - - public static void PushStyle() - { - if (_hasPushed) - PopStyle(); - - if (!ShouldUseTheme) - { - _hasPushed = false; - return; - } - - _hasPushed = true; - _pushedColorCount = 0; - _pushedStyleVarCount = 0; - - Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255)); - Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255)); - - Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248)); - Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66)); - Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248)); - - Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255)); - Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150)); - - Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255)); - Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255)); - - Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232)); - Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255)); - Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255)); - - Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255)); - Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255)); - Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255)); - Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255)); - - Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple")); - - Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255)); - Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255)); - - Push(ImGuiCol.Button, UIColors.Get("ButtonDefault")); - Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60)); - Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90)); - Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120)); - - Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121)); - Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple")); - Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive")); - Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255)); - Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive")); - - Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive")); - Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255)); - - Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255)); - - Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255)); - Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255)); - Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255)); - Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0)); - Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15)); - - Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255)); - Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255)); - - Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179)); - Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51)); - Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89)); - - PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)); - PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3)); - PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4)); - PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4)); - PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4)); - - PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f); - PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f); - PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f); - - PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f); - PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f); - - PushStyleVar(ImGuiStyleVar.WindowRounding, 7f); - PushStyleVar(ImGuiStyleVar.ChildRounding, 4f); - PushStyleVar(ImGuiStyleVar.FrameRounding, 4f); - PushStyleVar(ImGuiStyleVar.PopupRounding, 4f); - PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f); - PushStyleVar(ImGuiStyleVar.GrabRounding, 4f); - PushStyleVar(ImGuiStyleVar.TabRounding, 4f); - } - - public static void PopStyle() - { - if (!_hasPushed) - return; - - if (_pushedStyleVarCount > 0) - ImGui.PopStyleVar(_pushedStyleVarCount); - if (_pushedColorCount > 0) - ImGui.PopStyleColor(_pushedColorCount); - - _hasPushed = false; - _pushedColorCount = 0; - _pushedStyleVarCount = 0; - } - - private static void Push(ImGuiCol col, Vector4 rgba) - { - if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f) - rgba /= 255f; - - ImGui.PushStyleColor(col, rgba); - _pushedColorCount++; - } - - private static void Push(ImGuiCol col, uint packedRgba) - { - ImGui.PushStyleColor(col, packedRgba); - _pushedColorCount++; - } - - private static void PushStyleVar(ImGuiStyleVar var, float value) - { - ImGui.PushStyleVar(var, value); - _pushedStyleVarCount++; - } - - private static void PushStyleVar(ImGuiStyleVar var, Vector2 value) - { - ImGui.PushStyleVar(var, value); - _pushedStyleVarCount++; - } + _config = config; + _themeConfig = themeConfig; } + public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false; + + private static bool _hasPushed; + private static int _pushedColorCount; + private static int _pushedStyleVarCount; + + private static readonly StyleColorOption[] _colorOptions = + [ + new("color.text", "Text", () => Rgba(255, 255, 255, 255), ImGuiCol.Text), + new("color.textDisabled", "Text (Disabled)", () => Rgba(128, 128, 128, 255), ImGuiCol.TextDisabled), + new("color.windowBg", "Window Background", () => Rgba(23, 23, 23, 248), ImGuiCol.WindowBg), + new("color.childBg", "Child Background", () => Rgba(23, 23, 23, 66), ImGuiCol.ChildBg), + new("color.popupBg", "Popup Background", () => Rgba(23, 23, 23, 248), ImGuiCol.PopupBg), + new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border), + new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow), + new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg), + new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered), + new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive), + new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg), + new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive), + new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed), + new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg), + new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg), + new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab), + new("color.scrollbarGrabHovered", "Scrollbar Grab (Hover)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabHovered), + new("color.scrollbarGrabActive", "Scrollbar Grab (Active)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabActive), + new("color.checkMark", "Check Mark", () => UIColors.Get("LightlessPurple"), ImGuiCol.CheckMark, UiColorKey: "LightlessPurple"), + new("color.sliderGrab", "Slider Grab", () => Rgba(101, 101, 101, 255), ImGuiCol.SliderGrab), + new("color.sliderGrabActive", "Slider Grab (Active)", () => Rgba(123, 123, 123, 255), ImGuiCol.SliderGrabActive), + new("color.button", "Button", () => UIColors.Get("ButtonDefault"), ImGuiCol.Button, UiColorKey: "ButtonDefault"), + new("color.buttonHovered", "Button (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.ButtonHovered, UiColorKey: "LightlessPurple"), + new("color.buttonActive", "Button (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ButtonActive, UiColorKey: "LightlessPurpleActive"), + new("color.header", "Header", () => Rgba(0, 0, 0, 60), ImGuiCol.Header), + new("color.headerHovered", "Header (Hover)", () => Rgba(0, 0, 0, 90), ImGuiCol.HeaderHovered), + new("color.headerActive", "Header (Active)", () => Rgba(0, 0, 0, 120), ImGuiCol.HeaderActive), + new("color.separator", "Separator", () => Rgba(75, 75, 75, 121), ImGuiCol.Separator), + new("color.separatorHovered", "Separator (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.SeparatorHovered, UiColorKey: "LightlessPurple"), + new("color.separatorActive", "Separator (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.SeparatorActive, UiColorKey: "LightlessPurpleActive"), + new("color.resizeGrip", "Resize Grip", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGrip), + new("color.resizeGripHovered", "Resize Grip (Hover)", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGripHovered), + new("color.resizeGripActive", "Resize Grip (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ResizeGripActive, UiColorKey: "LightlessPurpleActive"), + new("color.tab", "Tab", () => Rgba(40, 40, 40, 255), ImGuiCol.Tab), + new("color.tabHovered", "Tab (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.TabHovered, UiColorKey: "LightlessPurple"), + new("color.tabActive", "Tab (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabActive, UiColorKey: "LightlessPurpleActive"), + new("color.tabUnfocused", "Tab (Unfocused)", () => Rgba(40, 40, 40, 255), ImGuiCol.TabUnfocused), + new("color.tabUnfocusedActive", "Tab (Unfocused Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabUnfocusedActive, UiColorKey: "LightlessPurpleActive"), + new("color.dockingPreview", "Docking Preview", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.DockingPreview, UiColorKey: "LightlessPurpleActive"), + new("color.dockingEmptyBg", "Docking Empty Background", () => Rgba(50, 50, 50, 255), ImGuiCol.DockingEmptyBg), + new("color.plotLines", "Plot Lines", () => Rgba(150, 150, 150, 255), ImGuiCol.PlotLines), + new("color.tableHeaderBg", "Table Header Background", () => Rgba(48, 48, 48, 255), ImGuiCol.TableHeaderBg), + new("color.tableBorderStrong", "Table Border Strong", () => Rgba(79, 79, 89, 255), ImGuiCol.TableBorderStrong), + new("color.tableBorderLight", "Table Border Light", () => Rgba(59, 59, 64, 255), ImGuiCol.TableBorderLight), + new("color.tableRowBg", "Table Row Background", () => Rgba(0, 0, 0, 0), ImGuiCol.TableRowBg), + new("color.tableRowBgAlt", "Table Row Background (Alt)", () => Rgba(255, 255, 255, 15), ImGuiCol.TableRowBgAlt), + new("color.textSelectedBg", "Text Selection Background", () => Rgba(173, 138, 245, 255), ImGuiCol.TextSelectedBg), + new("color.dragDropTarget", "Drag & Drop Target", () => Rgba(173, 138, 245, 255), ImGuiCol.DragDropTarget), + new("color.navHighlight", "Navigation Highlight", () => Rgba(173, 138, 245, 179), ImGuiCol.NavHighlight), + new("color.navWindowingDimBg", "Navigation Window Dim", () => Rgba(204, 204, 204, 51), ImGuiCol.NavWindowingDimBg), + new("color.navWindowingHighlight", "Navigation Window Highlight", () => Rgba(204, 204, 204, 89), ImGuiCol.NavWindowingHighlight) + ]; + + private static readonly StyleVector2Option[] _vector2Options = + [ + new("vector.windowPadding", "Window Padding", () => new Vector2(6f, 6f), ImGuiStyleVar.WindowPadding), + new("vector.framePadding", "Frame Padding", () => new Vector2(4f, 3f), ImGuiStyleVar.FramePadding), + new("vector.cellPadding", "Cell Padding", () => new Vector2(4f, 4f), ImGuiStyleVar.CellPadding), + new("vector.itemSpacing", "Item Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemSpacing), + new("vector.itemInnerSpacing", "Item Inner Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemInnerSpacing) + ]; + + private static readonly StyleFloatOption[] _floatOptions = + [ + new("float.indentSpacing", "Indent Spacing", 21f, ImGuiStyleVar.IndentSpacing, 0f, 100f, 0.5f), + new("float.scrollbarSize", "Scrollbar Size", 10f, ImGuiStyleVar.ScrollbarSize, 4f, 30f, 0.5f), + new("float.grabMinSize", "Grab Minimum Size", 20f, ImGuiStyleVar.GrabMinSize, 1f, 80f, 0.5f), + new("float.windowBorderSize", "Window Border Size", 1.5f, ImGuiStyleVar.WindowBorderSize, 0f, 5f, 0.1f), + new("float.childBorderSize", "Child Border Size", 1.5f, ImGuiStyleVar.ChildBorderSize, 0f, 5f, 0.1f), + new("float.popupBorderSize", "Popup Border Size", 1.5f, ImGuiStyleVar.PopupBorderSize, 0f, 5f, 0.1f), + new("float.frameBorderSize", "Frame Border Size", 0f, ImGuiStyleVar.FrameBorderSize, 0f, 5f, 0.1f), + new("float.windowRounding", "Window Rounding", 7f, ImGuiStyleVar.WindowRounding, 0f, 20f, 0.2f), + new("float.childRounding", "Child Rounding", 4f, ImGuiStyleVar.ChildRounding, 0f, 20f, 0.2f), + new("float.frameRounding", "Frame Rounding", 4f, ImGuiStyleVar.FrameRounding, 0f, 20f, 0.2f), + new("float.popupRounding", "Popup Rounding", 4f, ImGuiStyleVar.PopupRounding, 0f, 20f, 0.2f), + new("float.scrollbarRounding", "Scrollbar Rounding", 4f, ImGuiStyleVar.ScrollbarRounding, 0f, 20f, 0.2f), + new("float.grabRounding", "Grab Rounding", 4f, ImGuiStyleVar.GrabRounding, 0f, 20f, 0.2f), + new("float.tabRounding", "Tab Rounding", 4f, ImGuiStyleVar.TabRounding, 0f, 20f, 0.2f) + ]; + + public static IReadOnlyList ColorOptions => _colorOptions; + public static IReadOnlyList FloatOptions => _floatOptions; + public static IReadOnlyList Vector2Options => _vector2Options; + + public static void PushStyle() + { + if (_hasPushed) + PopStyle(); + + if (!ShouldUseTheme) + { + _hasPushed = false; + return; + } + + _hasPushed = true; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + + foreach (var option in _colorOptions) + Push(option.Target, ResolveColor(option)); + + foreach (var option in _vector2Options) + PushStyleVar(option.Target, ResolveVector(option)); + + foreach (var option in _floatOptions) + PushStyleVar(option.Target, ResolveFloat(option)); + } + + public static void PopStyle() + { + if (!_hasPushed) + return; + + if (_pushedStyleVarCount > 0) + ImGui.PopStyleVar(_pushedStyleVarCount); + if (_pushedColorCount > 0) + ImGui.PopStyleColor(_pushedColorCount); + + _hasPushed = false; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + } + + private static Vector4 ResolveColor(StyleColorOption option) + { + var defaultValue = NormalizeColorVector(option.DefaultValue()); + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed) + return PackedColorToVector4(packed); + + return defaultValue; + } + + private static Vector2 ResolveVector(StyleVector2Option option) + { + var value = option.DefaultValue(); + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Vector2 is { } vectorOverride) + { + value = vectorOverride; + } + + if (option.Min is { } min) + value = Vector2.Max(value, min); + if (option.Max is { } max) + value = Vector2.Min(value, max); + return value; + } + + private static float ResolveFloat(StyleFloatOption option) + { + var value = option.DefaultValue; + if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Float is { } floatOverride) + { + value = floatOverride; + } + + if (option.Min.HasValue) + value = MathF.Max(option.Min.Value, value); + if (option.Max.HasValue) + value = MathF.Min(option.Max.Value, value); + return value; + } + + private static void Push(ImGuiCol col, Vector4 rgba) + { + rgba = NormalizeColorVector(rgba); + ImGui.PushStyleColor(col, rgba); + _pushedColorCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, float value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, Vector2 value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + + private static Vector4 Rgba(byte r, byte g, byte b, byte a = 255) + => new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); + + internal static Vector4 NormalizeColorVector(Vector4 rgba) + { + if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f) + rgba /= 255f; + return rgba; + } + + internal static Vector4 PackedColorToVector4(uint color) + => new( + (color & 0xFF) / 255f, + ((color >> 8) & 0xFF) / 255f, + ((color >> 16) & 0xFF) / 255f, + ((color >> 24) & 0xFF) / 255f); }