feat(settings): convert the appearance tab, finishing the window

The last six stock collapsing headers are gone, so all seven tabs now read the
same way. Appearance is the one people open most, being where the themes live,
and it was still the odd one out.

Its four components each needed only the heading, so they get a SectionRenderer
rather than the whole widget set -- SettingsWidgets would have handed a colour
editor a Plugin reference it has no use for.

Three of the six could not take a literal key. The colour editor draws its
section seven times with seven titles, so the key becomes a parameter. The theme
picker keys its categories by position, since CategoryMap is a fixed list and
the index survives those names being translated later. And the custom-theme
section takes a fixed key although its label carries a count, or it would reset
every time a theme is imported or removed.

The theme picker's headers also pick up the disabled state properly now. They
sit inside an ImRaii.Disabled while a theme is being edited, and a draw-list
header cannot see that push -- so they had stayed at full contrast while
everything under them dimmed.
This commit is contained in:
2026-08-18 19:07:04 +02:00
parent 04f8f1ace8
commit d30922af8c
6 changed files with 116 additions and 19 deletions
+13 -4
View File
@@ -104,6 +104,11 @@ internal static class PluginHostFactory
// Transient: each surface owns its motes, so two backdrops on screen do
// not drift in lockstep.
services.AddSingleton(sp => new Ui.Components.Settings.SectionRenderer(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddTransient(sp => new Ui.StyleEngine.SurfaceBackdrop(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
@@ -176,10 +181,12 @@ internal static class PluginHostFactory
));
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ColorPicker(
sp.GetRequiredService<ThemeRegistry>()
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel(
sp.GetRequiredService<ThemeRegistry>(),
@@ -192,11 +199,13 @@ internal static class PluginHostFactory
));
services.AddSingleton(sp => new Ui.Components.Settings.FontsSection(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<FontManager>()
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ChatColourPicker(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ThemeRegistry>()
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.Components.Settings.SectionRenderer>()
));
services.AddSingleton(sp => new Ui.Components.Settings.Tabs.AppearanceTab(
sp.GetRequiredService<Ui.Components.Settings.ThemePicker>(),
@@ -17,11 +17,13 @@ internal sealed class ChatColourPicker
{
private readonly Plugin _plugin;
private readonly ThemeRegistry _themes;
private readonly SectionRenderer _sections;
private string? _applyDismissedFor;
private string? _lastSeenSlug;
public ChatColourPicker(Plugin plugin, ThemeRegistry themes)
public ChatColourPicker(Plugin plugin, ThemeRegistry themes, SectionRenderer sections)
{
_sections = sections;
_plugin = plugin;
_themes = themes;
}
@@ -32,7 +34,13 @@ internal sealed class ChatColourPicker
public void Draw()
{
if (!ImGui.CollapsingHeader(HellionStrings.Settings_Section_Colours))
if (
!_sections.Draw(
ImGui.GetID("appearance.chatcolours"u8),
HellionStrings.Settings_Section_Colours,
open: false
)
)
return;
DrawPresetButtons();
@@ -9,9 +9,11 @@ namespace HellionChat.Ui.Components.Settings;
internal sealed class ColorPicker
{
private readonly ThemeRegistry _themes;
private readonly SectionRenderer _sections;
public ColorPicker(ThemeRegistry themes)
public ColorPicker(ThemeRegistry themes, SectionRenderer sections)
{
_sections = sections;
_themes = themes;
}
@@ -89,6 +91,7 @@ internal sealed class ColorPicker
ImGui.Separator();
DrawSection(
ImGui.GetID("colors.surfaces"u8),
"Surfaces",
buffer,
c =>
@@ -112,6 +115,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.borders"u8),
"Borders",
buffer,
c => new[] { ("Border", c.Border) },
@@ -119,6 +123,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.text"u8),
"Text",
buffer,
c =>
@@ -138,6 +143,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.brand.primary"u8),
"Brand — Primary",
buffer,
c =>
@@ -159,6 +165,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.brand.accent"u8),
"Brand — Accent",
buffer,
c =>
@@ -178,6 +185,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.identity"u8),
"Identity",
buffer,
c => new[] { ("Identity", c.Identity) },
@@ -185,6 +193,7 @@ internal sealed class ColorPicker
);
DrawSection(
ImGui.GetID("colors.status"u8),
"Status",
buffer,
c =>
@@ -210,13 +219,17 @@ internal sealed class ColorPicker
}
private void DrawSection(
uint key,
string title,
Theme buffer,
Func<ThemeColors, (string label, uint color)[]> slots,
Func<ThemeColors, (string label, uint color)[], ThemeColors> writeBack
)
{
if (!ImGui.CollapsingHeader(title, ImGuiTreeNodeFlags.DefaultOpen))
// Key as a parameter: this runs seven times with seven titles, and
// ImGui would key each header off its label, so a translation could
// collapse two sections onto one shared state.
if (!_sections.Draw(key, title))
{
return;
}
@@ -15,9 +15,11 @@ internal sealed class FontsSection
{
private readonly Plugin _plugin;
private readonly FontManager _fontManager;
private readonly SectionRenderer _sections;
public FontsSection(Plugin plugin, FontManager fontManager)
public FontsSection(Plugin plugin, FontManager fontManager, SectionRenderer sections)
{
_sections = sections;
_plugin = plugin;
_fontManager = fontManager;
}
@@ -30,7 +32,13 @@ internal sealed class FontsSection
public void Draw()
{
if (!ImGui.CollapsingHeader(HellionStrings.Settings_Section_Fonts))
if (
!_sections.Draw(
ImGui.GetID("appearance.fonts"u8),
HellionStrings.Settings_Section_Fonts,
open: false
)
)
return;
// Readout so the user can see which font is actually active.
@@ -126,7 +134,13 @@ internal sealed class FontsSection
// ExtraGlyphRanges stays reachable regardless of the font source so the
// user can verify/override the per-language auto-activation (v1.5.3 note).
ImGui.Spacing();
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
if (
_sections.Draw(
ImGui.GetID("appearance.fonts.glyphs"u8),
Language.Options_ExtraGlyphs_Name,
open: false
)
)
{
ImGuiUtil.HelpMarker(
string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName)
@@ -0,0 +1,39 @@
using Dalamud.Bindings.ImGui;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Ui.StyleEngine.Widgets;
namespace HellionChat.Ui.Components.Settings;
// Section headings for the components that are not built out of setting rows --
// the theme picker, the colour editor, the font panel. They need the heading and
// nothing else from SettingsWidgets, and pulling that in would hand each of them
// a Plugin reference they have no use for.
internal sealed class SectionRenderer
{
private readonly ThemeRegistry _themes;
private readonly SettingsPalette _palette;
private int _frame = -1;
private SectionHeaderColors _colors;
internal SectionRenderer(ThemeRegistry themes, TokenResolver resolver)
{
_themes = themes;
_palette = new SettingsPalette(resolver);
}
// Key, not title. ImGui's own storage keys a collapsing header off its
// label, so translated titles would reset every open section on a language
// switch, and two categories translating alike would share one state.
internal bool Draw(uint key, string title, bool open = true, bool disabled = false)
{
if (_frame != ImGui.GetFrameCount())
{
_frame = ImGui.GetFrameCount();
_colors = _palette.Section(_themes.Active.Colors);
}
return SectionHeader.Draw(key, title, null, _colors, defaultOpen: open, disabled: disabled);
}
}
@@ -32,10 +32,12 @@ internal sealed class ThemePicker
private const float CardHeight = 132f;
private readonly ThemeRegistry _themes;
private readonly SectionRenderer _sections;
private readonly Plugin _plugin;
public ThemePicker(ThemeRegistry themes, Plugin plugin)
public ThemePicker(ThemeRegistry themes, Plugin plugin, SectionRenderer sections)
{
_sections = sections;
_themes = themes;
_plugin = plugin;
}
@@ -46,12 +48,20 @@ internal sealed class ThemePicker
using (ImRaii.Disabled(locked))
{
foreach (var (category, slugs, defaultExpanded) in CategoryMap)
// Keyed by position, not by name. CategoryMap is a fixed literal
// list, so the index is stable, and it survives the category names
// being translated later -- which the titles themselves would not.
for (var i = 0; i < CategoryMap.Length; i++)
{
var flags = defaultExpanded
? ImGuiTreeNodeFlags.DefaultOpen
: ImGuiTreeNodeFlags.None;
if (ImGui.CollapsingHeader(category, flags))
var (category, slugs, defaultExpanded) = CategoryMap[i];
if (
_sections.Draw(
ImGui.GetID($"theme.category.{i}"),
category,
open: defaultExpanded,
disabled: locked
)
)
{
DrawThemeGrid(Resolve(slugs));
}
@@ -62,10 +72,14 @@ internal sealed class ThemePicker
var customs = _themes.AllCustom().ToList();
if (customs.Count > 0)
{
// Fixed key although the label carries a count: keying off the
// text would reset the section every time a theme is imported
// or deleted.
if (
ImGui.CollapsingHeader(
_sections.Draw(
ImGui.GetID("theme.category.custom"u8),
$"Custom ({customs.Count})",
ImGuiTreeNodeFlags.DefaultOpen
disabled: locked
)
)
{